Refine editor action shell and add regression tests
This commit is contained in:
@@ -44,6 +44,7 @@ add_subdirectory(Rendering)
|
||||
add_subdirectory(rhi)
|
||||
add_subdirectory(resources)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(editor)
|
||||
|
||||
# ============================================================
|
||||
# Test Summary
|
||||
|
||||
36
tests/editor/CMakeLists.txt
Normal file
36
tests/editor/CMakeLists.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
|
||||
project(XCEngine_EditorTests)
|
||||
|
||||
set(EDITOR_TEST_SOURCES
|
||||
test_action_routing.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
|
||||
)
|
||||
|
||||
add_executable(editor_tests ${EDITOR_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(editor_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_tests PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
user32
|
||||
comdlg32
|
||||
)
|
||||
|
||||
target_include_directories(editor_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/editor/src
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(editor_tests)
|
||||
224
tests/editor/test_action_routing.cpp
Normal file
224
tests/editor/test_action_routing.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Actions/EditActionRouter.h"
|
||||
#include "Commands/EntityCommands.h"
|
||||
#include "Commands/SceneCommands.h"
|
||||
#include "Core/EditorContext.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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, 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, 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, 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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
Reference in New Issue
Block a user