910 lines
40 KiB
C++
910 lines
40 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include "Actions/EditActionRouter.h"
|
|
#include "Actions/HierarchyActionRouter.h"
|
|
#include "Actions/MainMenuActionRouter.h"
|
|
#include "Actions/ProjectActionRouter.h"
|
|
#include "Commands/EntityCommands.h"
|
|
#include "Commands/ProjectCommands.h"
|
|
#include "Commands/SceneCommands.h"
|
|
#include "Core/EditorContext.h"
|
|
#include "Core/PlaySessionController.h"
|
|
|
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
|
#include <XCEngine/Core/Math/Quaternion.h>
|
|
#include <XCEngine/Core/Math/Vector3.h>
|
|
#include <XCEngine/Resources/BuiltinResources.h>
|
|
#include <XCEngine/Resources/Model/Model.h>
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <string>
|
|
|
|
#ifdef _WIN32
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace XCEngine::Editor {
|
|
namespace {
|
|
|
|
fs::path GetRepositoryRoot() {
|
|
return fs::path(XCENGINE_EDITOR_REPO_ROOT);
|
|
}
|
|
|
|
std::string GetMeshFixturePath(const char* fileName) {
|
|
return (GetRepositoryRoot() / "tests" / "Fixtures" / "Resources" / "Mesh" / fileName).string();
|
|
}
|
|
|
|
void CopyTexturedTriangleFixture(const fs::path& assetsDir) {
|
|
fs::copy_file(
|
|
GetMeshFixturePath("textured_triangle.obj"),
|
|
assetsDir / "textured_triangle.obj",
|
|
fs::copy_options::overwrite_existing);
|
|
fs::copy_file(
|
|
GetMeshFixturePath("textured_triangle.mtl"),
|
|
assetsDir / "textured_triangle.mtl",
|
|
fs::copy_options::overwrite_existing);
|
|
fs::copy_file(
|
|
GetMeshFixturePath("checker.bmp"),
|
|
assetsDir / "checker.bmp",
|
|
fs::copy_options::overwrite_existing);
|
|
}
|
|
|
|
bool DirectoryHasEntries(const fs::path& directoryPath) {
|
|
std::error_code ec;
|
|
if (!fs::exists(directoryPath, ec) || !fs::is_directory(directoryPath, ec)) {
|
|
return false;
|
|
}
|
|
|
|
return fs::directory_iterator(directoryPath) != fs::directory_iterator();
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
struct AssimpDllGuard {
|
|
HMODULE module = nullptr;
|
|
|
|
~AssimpDllGuard() {
|
|
if (module != nullptr) {
|
|
FreeLibrary(module);
|
|
}
|
|
}
|
|
};
|
|
#endif
|
|
|
|
class EditorActionRoutingTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
m_projectRoot = fs::temp_directory_path() / ("xc_editor_tests_" + std::to_string(stamp));
|
|
fs::create_directories(m_projectRoot);
|
|
|
|
m_context.SetProjectPath(m_projectRoot.string());
|
|
m_context.GetProjectManager().Initialize(m_projectRoot.string());
|
|
m_context.GetSceneManager().NewScene("Editor Test Scene");
|
|
}
|
|
|
|
void TearDown() override {
|
|
std::error_code ec;
|
|
fs::remove_all(m_projectRoot, ec);
|
|
}
|
|
|
|
AssetItemPtr FindCurrentItemByName(const std::string& name) {
|
|
for (auto& item : m_context.GetProjectManager().GetCurrentItems()) {
|
|
if (item && item->name == name) {
|
|
return item;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int FindCurrentItemIndexByName(const std::string& name) {
|
|
auto& items = m_context.GetProjectManager().GetCurrentItems();
|
|
for (int i = 0; i < static_cast<int>(items.size()); ++i) {
|
|
if (items[i] && items[i]->name == name) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static void ExpectNear(const Math::Vector3& actual, const Math::Vector3& expected, float epsilon = 1e-4f) {
|
|
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
|
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
|
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
|
}
|
|
|
|
static size_t CountHierarchyEntities(const ISceneManager& sceneManager) {
|
|
auto countChildren = [](const ::XCEngine::Components::GameObject* gameObject, const auto& self) -> size_t {
|
|
if (!gameObject) {
|
|
return 0;
|
|
}
|
|
|
|
size_t count = 1;
|
|
for (size_t i = 0; i < gameObject->GetChildCount(); ++i) {
|
|
count += self(gameObject->GetChild(i), self);
|
|
}
|
|
return count;
|
|
};
|
|
|
|
size_t total = 0;
|
|
for (auto* root : sceneManager.GetRootEntities()) {
|
|
total += countChildren(root, countChildren);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
template <typename ComponentType>
|
|
static ::XCEngine::Components::GameObject* FindFirstEntityWithComponent(
|
|
::XCEngine::Components::GameObject* gameObject) {
|
|
if (gameObject == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (gameObject->GetComponent<ComponentType>() != nullptr) {
|
|
return gameObject;
|
|
}
|
|
|
|
for (size_t i = 0; i < gameObject->GetChildCount(); ++i) {
|
|
if (auto* found = FindFirstEntityWithComponent<ComponentType>(gameObject->GetChild(i))) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
EditorContext m_context;
|
|
fs::path m_projectRoot;
|
|
};
|
|
|
|
TEST_F(EditorActionRoutingTest, HierarchyRouteExecutesCopyPasteDuplicateDeleteAndRename) {
|
|
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "Original");
|
|
ASSERT_NE(entity, nullptr);
|
|
|
|
m_context.SetActiveActionRoute(EditorActionRoute::Hierarchy);
|
|
m_context.GetSelectionManager().SetSelectedEntity(entity->GetID());
|
|
|
|
const Actions::EditActionTarget target = Actions::ResolveEditActionTarget(m_context);
|
|
ASSERT_EQ(target.route, EditorActionRoute::Hierarchy);
|
|
ASSERT_EQ(target.selectedGameObject, entity);
|
|
|
|
uint64_t renameRequestedId = 0;
|
|
const uint64_t renameSubscription = m_context.GetEventBus().Subscribe<EntityRenameRequestedEvent>(
|
|
[&](const EntityRenameRequestedEvent& event) {
|
|
renameRequestedId = event.entityId;
|
|
});
|
|
EXPECT_TRUE(Actions::ExecuteRenameSelection(m_context, target));
|
|
EXPECT_EQ(renameRequestedId, entity->GetID());
|
|
m_context.GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(renameSubscription);
|
|
|
|
const size_t entityCountBeforePaste = CountHierarchyEntities(m_context.GetSceneManager());
|
|
EXPECT_TRUE(Actions::ExecuteCopySelection(m_context, target));
|
|
EXPECT_TRUE(m_context.GetSceneManager().HasClipboardData());
|
|
EXPECT_TRUE(Actions::ExecutePasteSelection(m_context, target));
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforePaste + 1);
|
|
|
|
const uint64_t pastedEntityId = m_context.GetSelectionManager().GetSelectedEntity();
|
|
EXPECT_NE(pastedEntityId, 0u);
|
|
EXPECT_NE(pastedEntityId, entity->GetID());
|
|
ASSERT_NE(m_context.GetSceneManager().GetEntity(pastedEntityId), nullptr);
|
|
EXPECT_EQ(m_context.GetSceneManager().GetEntity(pastedEntityId)->GetParent(), entity);
|
|
|
|
const Actions::EditActionTarget pastedTarget = Actions::ResolveEditActionTarget(m_context);
|
|
ASSERT_NE(pastedTarget.selectedGameObject, nullptr);
|
|
|
|
const size_t entityCountBeforeDuplicate = CountHierarchyEntities(m_context.GetSceneManager());
|
|
EXPECT_TRUE(Actions::ExecuteDuplicateSelection(m_context, pastedTarget));
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeDuplicate + 1);
|
|
|
|
const uint64_t duplicatedEntityId = m_context.GetSelectionManager().GetSelectedEntity();
|
|
const Actions::EditActionTarget duplicatedTarget = Actions::ResolveEditActionTarget(m_context);
|
|
ASSERT_NE(duplicatedTarget.selectedGameObject, nullptr);
|
|
EXPECT_EQ(duplicatedTarget.selectedGameObject->GetParent(), entity);
|
|
EXPECT_TRUE(Actions::ExecuteDeleteSelection(m_context, duplicatedTarget));
|
|
EXPECT_EQ(m_context.GetSceneManager().GetEntity(duplicatedEntityId), nullptr);
|
|
EXPECT_FALSE(m_context.GetSelectionManager().IsSelected(duplicatedEntityId));
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, CreatePrimitiveEntityAddsBuiltinMeshComponentsAndSupportsUndoRedo) {
|
|
using XCEngine::Resources::BuiltinPrimitiveType;
|
|
using XCEngine::Resources::GetBuiltinDefaultPrimitiveMaterialPath;
|
|
using XCEngine::Resources::GetBuiltinPrimitiveMeshPath;
|
|
|
|
auto* entity = Commands::CreatePrimitiveEntity(m_context, BuiltinPrimitiveType::Cube, nullptr);
|
|
ASSERT_NE(entity, nullptr);
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), 1u);
|
|
|
|
auto* meshFilter = entity->GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
|
auto* meshRenderer = entity->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
|
ASSERT_NE(meshFilter, nullptr);
|
|
ASSERT_NE(meshRenderer, nullptr);
|
|
EXPECT_EQ(meshFilter->GetMeshPath(), GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType::Cube).CStr());
|
|
ASSERT_EQ(meshRenderer->GetMaterialCount(), 1u);
|
|
EXPECT_EQ(meshRenderer->GetMaterialPath(0), GetBuiltinDefaultPrimitiveMaterialPath().CStr());
|
|
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
|
|
|
|
m_context.GetUndoManager().Undo();
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), 0u);
|
|
|
|
m_context.GetUndoManager().Redo();
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), 1u);
|
|
|
|
auto* restored = m_context.GetSceneManager().GetScene()->Find("Cube");
|
|
ASSERT_NE(restored, nullptr);
|
|
auto* restoredMeshFilter = restored->GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
|
auto* restoredMeshRenderer = restored->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
|
ASSERT_NE(restoredMeshFilter, nullptr);
|
|
ASSERT_NE(restoredMeshRenderer, nullptr);
|
|
EXPECT_EQ(restoredMeshFilter->GetMeshPath(), GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType::Cube).CStr());
|
|
ASSERT_EQ(restoredMeshRenderer->GetMaterialCount(), 1u);
|
|
EXPECT_EQ(restoredMeshRenderer->GetMaterialPath(0), GetBuiltinDefaultPrimitiveMaterialPath().CStr());
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectRouteExecutesOpenBackAndDelete) {
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
const fs::path folderPath = assetsDir / "RouteFolder";
|
|
const fs::path filePath = assetsDir / "DeleteMe.txt";
|
|
|
|
fs::create_directories(folderPath);
|
|
std::ofstream(filePath.string()) << "temporary";
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
const int folderIndex = FindCurrentItemIndexByName("RouteFolder");
|
|
ASSERT_GE(folderIndex, 0);
|
|
|
|
m_context.SetActiveActionRoute(EditorActionRoute::Project);
|
|
m_context.GetProjectManager().SetSelectedIndex(folderIndex);
|
|
|
|
const Actions::EditActionTarget folderTarget = Actions::ResolveEditActionTarget(m_context);
|
|
ASSERT_EQ(folderTarget.route, EditorActionRoute::Project);
|
|
ASSERT_NE(folderTarget.selectedAssetItem, nullptr);
|
|
EXPECT_EQ(folderTarget.selectedAssetItem->name, "RouteFolder");
|
|
|
|
EXPECT_TRUE(Actions::ExecuteOpenSelection(m_context, folderTarget));
|
|
EXPECT_EQ(m_context.GetProjectManager().GetCurrentPath(), "Assets/RouteFolder");
|
|
|
|
const Actions::EditActionTarget backTarget = Actions::ResolveEditActionTarget(m_context);
|
|
EXPECT_TRUE(Actions::ExecuteNavigateBackSelection(m_context, backTarget));
|
|
EXPECT_EQ(m_context.GetProjectManager().GetCurrentPath(), "Assets");
|
|
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
const int fileIndex = FindCurrentItemIndexByName("DeleteMe.txt");
|
|
ASSERT_GE(fileIndex, 0);
|
|
m_context.GetProjectManager().SetSelectedIndex(fileIndex);
|
|
|
|
const Actions::EditActionTarget deleteTarget = Actions::ResolveEditActionTarget(m_context);
|
|
ASSERT_NE(deleteTarget.selectedAssetItem, nullptr);
|
|
EXPECT_TRUE(Actions::ExecuteDeleteSelection(m_context, deleteTarget));
|
|
EXPECT_FALSE(fs::exists(filePath));
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsInstantiateModelAssetBuildsHierarchyAndSupportsUndoRedo) {
|
|
using ::XCEngine::Resources::ResourceManager;
|
|
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
CopyTexturedTriangleFixture(assetsDir);
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
const AssetItemPtr modelItem = FindCurrentItemByName("textured_triangle.obj");
|
|
ASSERT_NE(modelItem, nullptr);
|
|
EXPECT_EQ(modelItem->type, "Model");
|
|
EXPECT_TRUE(Commands::CanInstantiateModelAsset(m_context, modelItem));
|
|
|
|
#ifdef _WIN32
|
|
AssimpDllGuard dllGuard;
|
|
const fs::path assimpDllPath =
|
|
GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
|
ASSERT_TRUE(fs::exists(assimpDllPath));
|
|
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
|
ASSERT_NE(dllGuard.module, nullptr);
|
|
#endif
|
|
|
|
ResourceManager& resourceManager = ResourceManager::Get();
|
|
resourceManager.Initialize();
|
|
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
|
|
|
|
const size_t entityCountBeforeInstantiate = CountHierarchyEntities(m_context.GetSceneManager());
|
|
auto* createdRoot = Commands::InstantiateModelAsset(m_context, modelItem);
|
|
ASSERT_NE(createdRoot, nullptr);
|
|
const uint64_t createdRootId = createdRoot->GetID();
|
|
|
|
const size_t entityCountAfterInstantiate = CountHierarchyEntities(m_context.GetSceneManager());
|
|
EXPECT_GT(entityCountAfterInstantiate, entityCountBeforeInstantiate);
|
|
EXPECT_EQ(m_context.GetSelectionManager().GetSelectedEntity(), createdRootId);
|
|
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
|
|
|
|
auto* meshObject = FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(createdRoot);
|
|
ASSERT_NE(meshObject, nullptr);
|
|
auto* meshFilter = meshObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
|
auto* meshRenderer = meshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
|
ASSERT_NE(meshFilter, nullptr);
|
|
ASSERT_NE(meshRenderer, nullptr);
|
|
EXPECT_TRUE(meshFilter->GetMeshAssetRef().IsValid());
|
|
ASSERT_FALSE(meshRenderer->GetMaterialAssetRefs().empty());
|
|
EXPECT_TRUE(meshRenderer->GetMaterialAssetRefs()[0].IsValid());
|
|
|
|
m_context.GetUndoManager().Undo();
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeInstantiate);
|
|
EXPECT_EQ(m_context.GetSceneManager().GetEntity(createdRootId), nullptr);
|
|
EXPECT_FALSE(m_context.GetSelectionManager().HasSelection());
|
|
|
|
m_context.GetUndoManager().Redo();
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountAfterInstantiate);
|
|
EXPECT_EQ(m_context.GetSelectionManager().GetSelectedEntity(), createdRootId);
|
|
|
|
auto* restoredRoot = m_context.GetSceneManager().GetEntity(createdRootId);
|
|
ASSERT_NE(restoredRoot, nullptr);
|
|
auto* restoredMeshObject =
|
|
FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(restoredRoot);
|
|
ASSERT_NE(restoredMeshObject, nullptr);
|
|
auto* restoredMeshFilter =
|
|
restoredMeshObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
|
|
auto* restoredMeshRenderer =
|
|
restoredMeshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
|
ASSERT_NE(restoredMeshFilter, nullptr);
|
|
ASSERT_NE(restoredMeshRenderer, nullptr);
|
|
EXPECT_TRUE(restoredMeshFilter->GetMeshAssetRef().IsValid());
|
|
ASSERT_FALSE(restoredMeshRenderer->GetMaterialAssetRefs().empty());
|
|
EXPECT_TRUE(restoredMeshRenderer->GetMaterialAssetRefs()[0].IsValid());
|
|
|
|
resourceManager.UnloadAll();
|
|
resourceManager.SetResourceRoot("");
|
|
resourceManager.Shutdown();
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsInstantiateModelAssetAppliesSidecarMaterialOverrides) {
|
|
using ::XCEngine::Resources::Model;
|
|
using ::XCEngine::Resources::ResourceManager;
|
|
using ::XCEngine::Resources::ResourceType;
|
|
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
CopyTexturedTriangleFixture(assetsDir);
|
|
std::ofstream(assetsDir / "OverrideMaterial.mat")
|
|
<< "{\n"
|
|
" \"renderQueue\": \"geometry\"\n"
|
|
"}\n";
|
|
|
|
#ifdef _WIN32
|
|
AssimpDllGuard dllGuard;
|
|
const fs::path assimpDllPath =
|
|
GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
|
ASSERT_TRUE(fs::exists(assimpDllPath));
|
|
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
|
ASSERT_NE(dllGuard.module, nullptr);
|
|
#endif
|
|
|
|
ResourceManager& resourceManager = ResourceManager::Get();
|
|
resourceManager.Initialize();
|
|
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
|
|
|
|
const auto modelHandle = resourceManager.Load<Model>("Assets/textured_triangle.obj");
|
|
ASSERT_TRUE(modelHandle.IsValid());
|
|
ASSERT_FALSE(modelHandle->GetMeshBindings().Empty());
|
|
|
|
std::ofstream(assetsDir / "textured_triangle.obj.materialmap")
|
|
<< modelHandle->GetMeshBindings()[0].meshLocalID
|
|
<< "=Assets/OverrideMaterial.mat\n";
|
|
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
const AssetItemPtr modelItem = FindCurrentItemByName("textured_triangle.obj");
|
|
ASSERT_NE(modelItem, nullptr);
|
|
|
|
auto* createdRoot = Commands::InstantiateModelAsset(m_context, modelItem);
|
|
ASSERT_NE(createdRoot, nullptr);
|
|
|
|
auto* meshObject = FindFirstEntityWithComponent<::XCEngine::Components::MeshFilterComponent>(createdRoot);
|
|
ASSERT_NE(meshObject, nullptr);
|
|
auto* meshRenderer = meshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
|
|
ASSERT_NE(meshRenderer, nullptr);
|
|
|
|
::XCEngine::Resources::AssetRef overrideMaterialRef;
|
|
ASSERT_TRUE(resourceManager.TryGetAssetRef("Assets/OverrideMaterial.mat", ResourceType::Material, overrideMaterialRef));
|
|
ASSERT_FALSE(meshRenderer->GetMaterialAssetRefs().empty());
|
|
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].assetGuid, overrideMaterialRef.assetGuid);
|
|
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].localID, overrideMaterialRef.localID);
|
|
EXPECT_EQ(meshRenderer->GetMaterialAssetRefs()[0].resourceType, ResourceType::Material);
|
|
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Assets/OverrideMaterial.mat");
|
|
|
|
resourceManager.UnloadAll();
|
|
resourceManager.SetResourceRoot("");
|
|
resourceManager.Shutdown();
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, LoadSceneResetsSelectionAndUndoAfterFallbackSave) {
|
|
auto* savedEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Saved", "SavedEntity");
|
|
ASSERT_NE(savedEntity, nullptr);
|
|
ASSERT_TRUE(m_context.GetSelectionManager().HasSelection());
|
|
ASSERT_TRUE(m_context.GetUndoManager().CanUndo());
|
|
|
|
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "RegressionScene.xc";
|
|
EXPECT_TRUE(Commands::SaveDirtySceneWithFallback(m_context, savedScenePath.string()));
|
|
EXPECT_TRUE(fs::exists(savedScenePath));
|
|
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
|
|
auto* transientEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Transient", "TransientEntity");
|
|
ASSERT_NE(transientEntity, nullptr);
|
|
ASSERT_TRUE(m_context.GetSelectionManager().HasSelection());
|
|
ASSERT_TRUE(m_context.GetUndoManager().CanUndo());
|
|
|
|
EXPECT_TRUE(Commands::LoadScene(m_context, savedScenePath.string(), false));
|
|
EXPECT_FALSE(m_context.GetSelectionManager().HasSelection());
|
|
EXPECT_FALSE(m_context.GetUndoManager().CanUndo());
|
|
ASSERT_EQ(m_context.GetSceneManager().GetRootEntities().size(), 1u);
|
|
EXPECT_EQ(m_context.GetSceneManager().GetRootEntities()[0]->GetName(), "SavedEntity");
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, NewSceneResetsSelectionAndUndoForCleanScene) {
|
|
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "BeforeNewScene");
|
|
ASSERT_NE(entity, nullptr);
|
|
ASSERT_TRUE(m_context.GetSelectionManager().HasSelection());
|
|
ASSERT_TRUE(m_context.GetUndoManager().CanUndo());
|
|
|
|
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "BeforeNewScene.xc";
|
|
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
|
|
ASSERT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
|
|
EXPECT_TRUE(Commands::NewScene(m_context, "Fresh Scene"));
|
|
EXPECT_TRUE(m_context.GetSceneManager().HasActiveScene());
|
|
EXPECT_TRUE(m_context.GetSceneManager().IsSceneDirty());
|
|
EXPECT_EQ(m_context.GetSceneManager().GetCurrentSceneName(), "Fresh Scene");
|
|
EXPECT_TRUE(m_context.GetSceneManager().GetCurrentScenePath().empty());
|
|
EXPECT_TRUE(m_context.GetSceneManager().GetRootEntities().empty());
|
|
EXPECT_FALSE(m_context.GetSelectionManager().HasSelection());
|
|
EXPECT_FALSE(m_context.GetUndoManager().CanUndo());
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, SaveDirtySceneWithFallbackDoesNothingWhenSceneIsAlreadyClean) {
|
|
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "CleanScene.xc";
|
|
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
|
|
ASSERT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
|
|
const fs::path unusedFallbackPath = m_projectRoot / "Assets" / "Scenes" / "UnusedFallback.xc";
|
|
EXPECT_TRUE(Commands::SaveDirtySceneWithFallback(m_context, unusedFallbackPath.string()));
|
|
EXPECT_TRUE(fs::exists(savedScenePath));
|
|
EXPECT_FALSE(fs::exists(unusedFallbackPath));
|
|
EXPECT_EQ(fs::path(m_context.GetSceneManager().GetCurrentScenePath()), savedScenePath);
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ReparentPreserveWorldTransformKeepsWorldPose) {
|
|
auto* parentA = Commands::CreateEmptyEntity(m_context, nullptr, "Create Parent A", "ParentA");
|
|
auto* child = Commands::CreateEmptyEntity(m_context, parentA, "Create Child", "Child");
|
|
auto* parentB = Commands::CreateEmptyEntity(m_context, nullptr, "Create Parent B", "ParentB");
|
|
|
|
ASSERT_NE(parentA, nullptr);
|
|
ASSERT_NE(child, nullptr);
|
|
ASSERT_NE(parentB, nullptr);
|
|
|
|
parentA->GetTransform()->SetPosition(Math::Vector3(5.0f, 1.0f, -2.0f));
|
|
parentB->GetTransform()->SetPosition(Math::Vector3(-4.0f, 3.0f, 8.0f));
|
|
child->GetTransform()->SetLocalPosition(Math::Vector3(2.0f, 3.0f, 4.0f));
|
|
child->GetTransform()->SetLocalRotation(Math::Quaternion::FromEulerAngles(Math::Vector3(0.25f, 0.5f, 0.75f)));
|
|
child->GetTransform()->SetLocalScale(Math::Vector3(1.5f, 2.0f, 0.5f));
|
|
|
|
const Math::Vector3 worldPositionBefore = child->GetTransform()->GetPosition();
|
|
const Math::Vector3 worldScaleBefore = child->GetTransform()->GetScale();
|
|
|
|
EXPECT_TRUE(Commands::CanReparentEntity(child, parentB));
|
|
EXPECT_FALSE(Commands::CanReparentEntity(parentA, child));
|
|
EXPECT_TRUE(Commands::ReparentEntityPreserveWorldTransform(m_context, child, parentB->GetID()));
|
|
|
|
EXPECT_EQ(child->GetParent(), parentB);
|
|
ExpectNear(child->GetTransform()->GetPosition(), worldPositionBefore);
|
|
ExpectNear(child->GetTransform()->GetScale(), worldScaleBefore);
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsExitResetAndAboutPopup) {
|
|
int exitRequestCount = 0;
|
|
int resetLayoutCount = 0;
|
|
|
|
const uint64_t exitSubscription = m_context.GetEventBus().Subscribe<EditorExitRequestedEvent>(
|
|
[&](const EditorExitRequestedEvent&) {
|
|
++exitRequestCount;
|
|
});
|
|
const uint64_t resetSubscription = m_context.GetEventBus().Subscribe<DockLayoutResetRequestedEvent>(
|
|
[&](const DockLayoutResetRequestedEvent&) {
|
|
++resetLayoutCount;
|
|
});
|
|
|
|
UI::DeferredPopupState aboutPopup;
|
|
EXPECT_FALSE(aboutPopup.HasPendingOpenRequest());
|
|
|
|
Actions::RequestAboutPopup(aboutPopup);
|
|
Actions::RequestDockLayoutReset(m_context);
|
|
Actions::RequestEditorExit(m_context);
|
|
|
|
EXPECT_TRUE(aboutPopup.HasPendingOpenRequest());
|
|
EXPECT_EQ(resetLayoutCount, 1);
|
|
EXPECT_EQ(exitRequestCount, 1);
|
|
|
|
m_context.GetEventBus().Unsubscribe<EditorExitRequestedEvent>(exitSubscription);
|
|
m_context.GetEventBus().Unsubscribe<DockLayoutResetRequestedEvent>(resetSubscription);
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsPlayPauseResumeAndStepEvents) {
|
|
int playStartRequestCount = 0;
|
|
int playStopRequestCount = 0;
|
|
int playPauseRequestCount = 0;
|
|
int playResumeRequestCount = 0;
|
|
int playStepRequestCount = 0;
|
|
|
|
const uint64_t playStartSubscription = m_context.GetEventBus().Subscribe<PlayModeStartRequestedEvent>(
|
|
[&](const PlayModeStartRequestedEvent&) {
|
|
++playStartRequestCount;
|
|
});
|
|
const uint64_t playStopSubscription = m_context.GetEventBus().Subscribe<PlayModeStopRequestedEvent>(
|
|
[&](const PlayModeStopRequestedEvent&) {
|
|
++playStopRequestCount;
|
|
});
|
|
const uint64_t playPauseSubscription = m_context.GetEventBus().Subscribe<PlayModePauseRequestedEvent>(
|
|
[&](const PlayModePauseRequestedEvent&) {
|
|
++playPauseRequestCount;
|
|
});
|
|
const uint64_t playResumeSubscription = m_context.GetEventBus().Subscribe<PlayModeResumeRequestedEvent>(
|
|
[&](const PlayModeResumeRequestedEvent&) {
|
|
++playResumeRequestCount;
|
|
});
|
|
const uint64_t playStepSubscription = m_context.GetEventBus().Subscribe<PlayModeStepRequestedEvent>(
|
|
[&](const PlayModeStepRequestedEvent&) {
|
|
++playStepRequestCount;
|
|
});
|
|
|
|
Actions::RequestTogglePlayMode(m_context);
|
|
EXPECT_EQ(playStartRequestCount, 1);
|
|
EXPECT_EQ(playStopRequestCount, 0);
|
|
|
|
m_context.SetRuntimeMode(EditorRuntimeMode::Play);
|
|
Actions::RequestTogglePauseMode(m_context);
|
|
EXPECT_EQ(playPauseRequestCount, 1);
|
|
EXPECT_EQ(playResumeRequestCount, 0);
|
|
Actions::RequestStepPlayMode(m_context);
|
|
EXPECT_EQ(playStepRequestCount, 0);
|
|
|
|
m_context.SetRuntimeMode(EditorRuntimeMode::Paused);
|
|
Actions::RequestTogglePauseMode(m_context);
|
|
EXPECT_EQ(playResumeRequestCount, 1);
|
|
Actions::RequestStepPlayMode(m_context);
|
|
EXPECT_EQ(playStepRequestCount, 1);
|
|
|
|
Actions::RequestTogglePlayMode(m_context);
|
|
EXPECT_EQ(playStopRequestCount, 1);
|
|
|
|
m_context.SetRuntimeMode(EditorRuntimeMode::Edit);
|
|
Actions::RequestStepPlayMode(m_context);
|
|
EXPECT_EQ(playStepRequestCount, 1);
|
|
|
|
m_context.GetEventBus().Unsubscribe<PlayModeStartRequestedEvent>(playStartSubscription);
|
|
m_context.GetEventBus().Unsubscribe<PlayModeStopRequestedEvent>(playStopSubscription);
|
|
m_context.GetEventBus().Unsubscribe<PlayModePauseRequestedEvent>(playPauseSubscription);
|
|
m_context.GetEventBus().Unsubscribe<PlayModeResumeRequestedEvent>(playResumeSubscription);
|
|
m_context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(playStepSubscription);
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsReportWhenScriptAssembliesCanBeRebuilt) {
|
|
EXPECT_TRUE(Commands::CanRebuildScriptAssemblies(m_context));
|
|
|
|
m_context.SetRuntimeMode(EditorRuntimeMode::Play);
|
|
EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context));
|
|
|
|
m_context.SetRuntimeMode(EditorRuntimeMode::Edit);
|
|
m_context.SetProjectPath(std::string());
|
|
EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context));
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) {
|
|
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc";
|
|
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
|
|
ASSERT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
|
|
PlaySessionController controller;
|
|
ASSERT_TRUE(controller.StartPlay(m_context));
|
|
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play);
|
|
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDocumentDirtyTrackingEnabled());
|
|
|
|
EXPECT_FALSE(Commands::NewScene(m_context, "Blocked During Play"));
|
|
EXPECT_FALSE(Commands::SaveCurrentScene(m_context));
|
|
|
|
const size_t entityCountBeforeCreate = CountHierarchyEntities(m_context.GetSceneManager());
|
|
auto* runtimeEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Runtime Entity", "RuntimeOnly");
|
|
ASSERT_NE(runtimeEntity, nullptr);
|
|
const uint64_t runtimeEntityId = runtimeEntity->GetID();
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate + 1);
|
|
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
|
|
const Actions::ActionBinding undoAction = Actions::MakeUndoAction(m_context);
|
|
EXPECT_TRUE(undoAction.enabled);
|
|
Actions::ExecuteUndo(m_context);
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate);
|
|
EXPECT_EQ(m_context.GetSceneManager().GetEntity(runtimeEntityId), nullptr);
|
|
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
|
|
const Actions::ActionBinding redoAction = Actions::MakeRedoAction(m_context);
|
|
EXPECT_TRUE(redoAction.enabled);
|
|
Actions::ExecuteRedo(m_context);
|
|
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate + 1);
|
|
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
|
|
ASSERT_TRUE(controller.StopPlay(m_context));
|
|
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit);
|
|
EXPECT_TRUE(m_context.GetSceneManager().IsSceneDocumentDirtyTrackingEnabled());
|
|
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, HierarchyRouterRenameHelpersPublishAndCommit) {
|
|
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "BeforeRename");
|
|
ASSERT_NE(entity, nullptr);
|
|
|
|
uint64_t renameRequestedId = 0;
|
|
const uint64_t renameSubscription = m_context.GetEventBus().Subscribe<EntityRenameRequestedEvent>(
|
|
[&](const EntityRenameRequestedEvent& event) {
|
|
renameRequestedId = event.entityId;
|
|
});
|
|
|
|
Actions::RequestEntityRename(m_context, entity);
|
|
EXPECT_EQ(renameRequestedId, entity->GetID());
|
|
|
|
EXPECT_TRUE(Actions::CommitEntityRename(m_context, entity->GetID(), "AfterRename"));
|
|
ASSERT_NE(m_context.GetSceneManager().GetEntity(entity->GetID()), nullptr);
|
|
EXPECT_EQ(m_context.GetSceneManager().GetEntity(entity->GetID())->GetName(), "AfterRename");
|
|
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
|
|
|
|
EXPECT_FALSE(Actions::CommitEntityRename(m_context, entity->GetID(), ""));
|
|
EXPECT_FALSE(Actions::CommitEntityRename(m_context, 0, "Invalid"));
|
|
|
|
m_context.GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(renameSubscription);
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, CreateTypedLightCommandsAssignExpectedNamesAndTypes) {
|
|
auto* directionalLight = Commands::CreateDirectionalLightEntity(m_context);
|
|
auto* pointLight = Commands::CreatePointLightEntity(m_context);
|
|
auto* spotLight = Commands::CreateSpotLightEntity(m_context);
|
|
|
|
ASSERT_NE(directionalLight, nullptr);
|
|
ASSERT_NE(pointLight, nullptr);
|
|
ASSERT_NE(spotLight, nullptr);
|
|
|
|
EXPECT_EQ(directionalLight->GetName(), "Directional Light");
|
|
EXPECT_EQ(pointLight->GetName(), "Point Light");
|
|
EXPECT_EQ(spotLight->GetName(), "Spot Light");
|
|
|
|
auto* directionalComponent = directionalLight->GetComponent<Components::LightComponent>();
|
|
auto* pointComponent = pointLight->GetComponent<Components::LightComponent>();
|
|
auto* spotComponent = spotLight->GetComponent<Components::LightComponent>();
|
|
|
|
ASSERT_NE(directionalComponent, nullptr);
|
|
ASSERT_NE(pointComponent, nullptr);
|
|
ASSERT_NE(spotComponent, nullptr);
|
|
|
|
EXPECT_EQ(directionalComponent->GetLightType(), Components::LightType::Directional);
|
|
EXPECT_EQ(pointComponent->GetLightType(), Components::LightType::Point);
|
|
EXPECT_EQ(spotComponent->GetLightType(), Components::LightType::Spot);
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, HierarchyItemContextRequestSelectsEntityAndStoresPopupTarget) {
|
|
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "ContextTarget");
|
|
ASSERT_NE(entity, nullptr);
|
|
|
|
UI::TargetedPopupState<::XCEngine::Components::GameObject*> popupState;
|
|
EXPECT_FALSE(popupState.HasTarget());
|
|
EXPECT_FALSE(popupState.HasPendingOpenRequest());
|
|
|
|
Actions::HandleHierarchyItemContextRequest(m_context, entity, popupState);
|
|
|
|
EXPECT_EQ(m_context.GetSelectionManager().GetSelectedEntity(), entity->GetID());
|
|
ASSERT_TRUE(popupState.HasTarget());
|
|
EXPECT_EQ(popupState.TargetValue(), entity);
|
|
EXPECT_TRUE(popupState.HasPendingOpenRequest());
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsCreateFolderMoveAssetAndOpenFolderHelper) {
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
const fs::path sourceFilePath = assetsDir / "MoveMe.txt";
|
|
|
|
std::ofstream(sourceFilePath.string()) << "move source";
|
|
|
|
const AssetItemPtr folderItem = Commands::CreateFolder(m_context.GetProjectManager(), "MovedFolder");
|
|
ASSERT_NE(folderItem, nullptr);
|
|
ASSERT_TRUE(folderItem->isFolder);
|
|
|
|
EXPECT_TRUE(Commands::MoveAssetToFolder(m_context.GetProjectManager(), sourceFilePath.string(), folderItem));
|
|
EXPECT_FALSE(fs::exists(sourceFilePath));
|
|
EXPECT_TRUE(fs::exists(assetsDir / "MovedFolder" / "MoveMe.txt"));
|
|
|
|
EXPECT_TRUE(Actions::OpenProjectAsset(m_context, folderItem));
|
|
EXPECT_EQ(m_context.GetProjectManager().GetCurrentPath(), "Assets/MovedFolder");
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsCreateFolderUsesUniqueDefaultName) {
|
|
const AssetItemPtr firstFolder = Commands::CreateFolder(m_context.GetProjectManager(), "New Folder");
|
|
const AssetItemPtr secondFolder = Commands::CreateFolder(m_context.GetProjectManager(), "New Folder");
|
|
|
|
ASSERT_NE(firstFolder, nullptr);
|
|
ASSERT_NE(secondFolder, nullptr);
|
|
EXPECT_NE(firstFolder->fullPath, secondFolder->fullPath);
|
|
EXPECT_TRUE(fs::exists(m_projectRoot / "Assets" / "New Folder"));
|
|
EXPECT_TRUE(fs::exists(m_projectRoot / "Assets" / "New Folder 1"));
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsRenameAssetUpdatesSelectionAndPreservesFileExtension) {
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
const fs::path originalPath = assetsDir / "RenameMe.txt";
|
|
std::ofstream(originalPath.string()) << "rename target";
|
|
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
const AssetItemPtr item = FindCurrentItemByName("RenameMe.txt");
|
|
ASSERT_NE(item, nullptr);
|
|
m_context.GetProjectManager().SetSelectedItem(item);
|
|
|
|
EXPECT_TRUE(Commands::RenameAsset(m_context.GetProjectManager(), item, "RenamedAsset"));
|
|
EXPECT_FALSE(fs::exists(originalPath));
|
|
EXPECT_TRUE(fs::exists(assetsDir / "RenamedAsset.txt"));
|
|
|
|
const AssetItemPtr renamedItem = FindCurrentItemByName("RenamedAsset.txt");
|
|
ASSERT_NE(renamedItem, nullptr);
|
|
EXPECT_EQ(m_context.GetProjectManager().GetSelectedItemPath(), renamedItem->fullPath);
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectItemContextRequestSelectsAssetAndStoresPopupTarget) {
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
const fs::path filePath = assetsDir / "ContextAsset.txt";
|
|
std::ofstream(filePath.string()) << "context asset";
|
|
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
const AssetItemPtr item = FindCurrentItemByName("ContextAsset.txt");
|
|
ASSERT_NE(item, nullptr);
|
|
|
|
UI::TargetedPopupState<AssetItemPtr> popupState;
|
|
EXPECT_FALSE(popupState.HasTarget());
|
|
EXPECT_FALSE(popupState.HasPendingOpenRequest());
|
|
|
|
Actions::HandleProjectItemContextRequest(m_context.GetProjectManager(), item, popupState);
|
|
|
|
EXPECT_EQ(m_context.GetProjectManager().GetSelectedItemPath(), item->fullPath);
|
|
ASSERT_TRUE(popupState.HasTarget());
|
|
EXPECT_EQ(popupState.TargetValue(), item);
|
|
EXPECT_TRUE(popupState.HasPendingOpenRequest());
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsRejectInvalidMoveTargets) {
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
const fs::path sourceFilePath = assetsDir / "MoveSource.txt";
|
|
const fs::path targetFolderPath = assetsDir / "TargetFolder";
|
|
const fs::path plainFilePath = assetsDir / "PlainFile.txt";
|
|
|
|
std::ofstream(sourceFilePath.string()) << "move source";
|
|
std::ofstream(plainFilePath.string()) << "not a folder";
|
|
fs::create_directories(targetFolderPath);
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
const AssetItemPtr targetFolder = FindCurrentItemByName("TargetFolder");
|
|
const AssetItemPtr plainFile = FindCurrentItemByName("PlainFile.txt");
|
|
ASSERT_NE(targetFolder, nullptr);
|
|
ASSERT_TRUE(targetFolder->isFolder);
|
|
ASSERT_NE(plainFile, nullptr);
|
|
ASSERT_FALSE(plainFile->isFolder);
|
|
|
|
EXPECT_FALSE(Commands::MoveAssetToFolder(m_context.GetProjectManager(), "", targetFolder));
|
|
EXPECT_FALSE(Commands::MoveAssetToFolder(m_context.GetProjectManager(), sourceFilePath.string(), nullptr));
|
|
EXPECT_FALSE(Commands::MoveAssetToFolder(m_context.GetProjectManager(), sourceFilePath.string(), plainFile));
|
|
EXPECT_FALSE(Commands::MoveAssetToFolder(m_context.GetProjectManager(), targetFolder->fullPath, targetFolder));
|
|
EXPECT_TRUE(fs::exists(sourceFilePath));
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectSelectionSurvivesRefreshWhenItemOrderChanges) {
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
const fs::path selectedPath = assetsDir / "Selected.txt";
|
|
const fs::path earlierPath = assetsDir / "Earlier.txt";
|
|
|
|
std::ofstream(selectedPath.string()) << "selected";
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
const int selectedIndex = FindCurrentItemIndexByName("Selected.txt");
|
|
ASSERT_GE(selectedIndex, 0);
|
|
m_context.GetProjectManager().SetSelectedIndex(selectedIndex);
|
|
|
|
AssetItemPtr selectedItem = Actions::GetSelectedAssetItem(m_context);
|
|
ASSERT_NE(selectedItem, nullptr);
|
|
EXPECT_EQ(selectedItem->name, "Selected.txt");
|
|
|
|
std::ofstream(earlierPath.string()) << "earlier";
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
selectedItem = Actions::GetSelectedAssetItem(m_context);
|
|
ASSERT_NE(selectedItem, nullptr);
|
|
EXPECT_EQ(selectedItem->name, "Selected.txt");
|
|
EXPECT_EQ(
|
|
m_context.GetProjectManager().GetSelectedIndex(),
|
|
FindCurrentItemIndexByName("Selected.txt"));
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsExposeAssetCacheMaintenanceActions) {
|
|
using ::XCEngine::Resources::ResourceManager;
|
|
|
|
const fs::path materialPath = m_projectRoot / "Assets" / "ToolMaterial.mat";
|
|
std::ofstream(materialPath.string()) << "{\n \"renderQueue\": \"geometry\"\n}\n";
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
ResourceManager& resourceManager = ResourceManager::Get();
|
|
resourceManager.Initialize();
|
|
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
|
|
|
|
const AssetItemPtr materialItem = FindCurrentItemByName("ToolMaterial.mat");
|
|
ASSERT_NE(materialItem, nullptr);
|
|
m_context.GetProjectManager().SetSelectedItem(materialItem);
|
|
|
|
EXPECT_TRUE(Commands::CanReimportSelectedAsset(m_context));
|
|
EXPECT_TRUE(Commands::CanReimportAllAssets(m_context));
|
|
EXPECT_TRUE(Commands::CanClearLibrary(m_context));
|
|
|
|
m_context.SetRuntimeMode(EditorRuntimeMode::Play);
|
|
EXPECT_FALSE(Commands::CanReimportSelectedAsset(m_context));
|
|
EXPECT_FALSE(Commands::CanReimportAllAssets(m_context));
|
|
EXPECT_FALSE(Commands::CanClearLibrary(m_context));
|
|
|
|
m_context.SetRuntimeMode(EditorRuntimeMode::Edit);
|
|
resourceManager.SetResourceRoot("");
|
|
resourceManager.Shutdown();
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsReimportSelectedAssetAndClearLibraryDriveAssetCache) {
|
|
using ::XCEngine::Resources::ResourceManager;
|
|
|
|
const fs::path materialPath = m_projectRoot / "Assets" / "ToolMaterial.mat";
|
|
std::ofstream(materialPath.string()) << "{\n \"renderQueue\": \"geometry\"\n}\n";
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
ResourceManager& resourceManager = ResourceManager::Get();
|
|
resourceManager.Initialize();
|
|
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
|
|
|
|
const AssetItemPtr materialItem = FindCurrentItemByName("ToolMaterial.mat");
|
|
ASSERT_NE(materialItem, nullptr);
|
|
m_context.GetProjectManager().SetSelectedItem(materialItem);
|
|
|
|
const fs::path libraryRoot(resourceManager.GetProjectLibraryRoot().CStr());
|
|
EXPECT_TRUE(Commands::ReimportSelectedAsset(m_context));
|
|
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
EXPECT_TRUE(Commands::ClearLibrary(m_context));
|
|
EXPECT_FALSE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
EXPECT_TRUE(Commands::ReimportAllAssets(m_context));
|
|
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
|
|
|
|
resourceManager.SetResourceRoot("");
|
|
resourceManager.Shutdown();
|
|
}
|
|
|
|
TEST_F(EditorActionRoutingTest, ProjectCommandsRejectMovingFolderIntoItsDescendant) {
|
|
const fs::path assetsDir = m_projectRoot / "Assets";
|
|
const fs::path parentPath = assetsDir / "Parent";
|
|
const fs::path childPath = parentPath / "Child";
|
|
|
|
fs::create_directories(childPath);
|
|
m_context.GetProjectManager().RefreshCurrentFolder();
|
|
|
|
const AssetItemPtr parentFolder = FindCurrentItemByName("Parent");
|
|
ASSERT_NE(parentFolder, nullptr);
|
|
ASSERT_TRUE(parentFolder->isFolder);
|
|
|
|
m_context.GetProjectManager().NavigateToFolder(parentFolder);
|
|
const AssetItemPtr childFolder = FindCurrentItemByName("Child");
|
|
ASSERT_NE(childFolder, nullptr);
|
|
ASSERT_TRUE(childFolder->isFolder);
|
|
|
|
EXPECT_FALSE(Commands::MoveAssetToFolder(m_context.GetProjectManager(), parentFolder->fullPath, childFolder));
|
|
EXPECT_TRUE(fs::exists(parentPath));
|
|
EXPECT_TRUE(fs::exists(childPath));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace XCEngine::Editor
|