Build XCUI splitter foundation and test harness
This commit is contained in:
17
tests/UI/Core/CMakeLists.txt
Normal file
17
tests/UI/Core/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
|
||||
project(XCEngine_CoreUITests)
|
||||
|
||||
add_subdirectory(unit)
|
||||
add_subdirectory(integration)
|
||||
|
||||
add_custom_target(core_ui_unit_tests
|
||||
DEPENDS
|
||||
core_ui_tests
|
||||
)
|
||||
|
||||
add_custom_target(core_ui_all_tests
|
||||
DEPENDS
|
||||
core_ui_unit_tests
|
||||
core_ui_integration_tests
|
||||
)
|
||||
1
tests/UI/Core/integration/CMakeLists.txt
Normal file
1
tests/UI/Core/integration/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
||||
add_custom_target(core_ui_integration_tests)
|
||||
8
tests/UI/Core/integration/README.md
Normal file
8
tests/UI/Core/integration/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Core UI Integration Notes
|
||||
|
||||
The core XCUI lane currently validates shared primitives through automated unit tests.
|
||||
|
||||
Interactive validation belongs to:
|
||||
|
||||
- `tests/UI/Runtime/integration/` for game/runtime UI
|
||||
- `tests/UI/Editor/integration/` for editor UI
|
||||
40
tests/UI/Core/unit/CMakeLists.txt
Normal file
40
tests/UI/Core/unit/CMakeLists.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
set(CORE_UI_TEST_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_shortcut_scope.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_splitter_layout.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_splitter_interaction.cpp
|
||||
# Migration bridge: legacy XCUI unit coverage still lives under tests/Core/UI
|
||||
# until it is moved into tests/UI/Core/unit without changing behavior.
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_core.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_expansion_model.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_flat_hierarchy_helpers.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_input_dispatcher.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_keyboard_navigation_model.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_property_edit_model.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_layout_engine.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_selection_model.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_text_editing.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_text_input_controller.cpp
|
||||
)
|
||||
|
||||
add_executable(core_ui_tests ${CORE_UI_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(core_ui_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(core_ui_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(core_ui_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(core_ui_tests)
|
||||
171
tests/UI/Core/unit/test_ui_shortcut_scope.cpp
Normal file
171
tests/UI/Core/unit/test_ui_shortcut_scope.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||
#include <XCEngine/UI/Input/UIShortcutRegistry.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputDispatchDecision;
|
||||
using XCEngine::UI::UIInputDispatcher;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIInputPath;
|
||||
using XCEngine::UI::UIShortcutBinding;
|
||||
using XCEngine::UI::UIShortcutContext;
|
||||
using XCEngine::UI::UIShortcutRegistry;
|
||||
using XCEngine::UI::UIShortcutScope;
|
||||
|
||||
UIInputEvent MakeCtrlPEvent() {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
event.modifiers.control = true;
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIShortcutScopeTest, RegistryUsesCommandScopeInsteadOfActivePathForShortcutResolution) {
|
||||
UIShortcutRegistry registry = {};
|
||||
|
||||
UIShortcutBinding dragWidgetBinding = {};
|
||||
dragWidgetBinding.scope = UIShortcutScope::Widget;
|
||||
dragWidgetBinding.ownerId = 91u;
|
||||
dragWidgetBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
dragWidgetBinding.chord.modifiers.control = true;
|
||||
dragWidgetBinding.commandId = "drag.widget.command";
|
||||
registry.RegisterBinding(dragWidgetBinding);
|
||||
|
||||
UIShortcutBinding panelBinding = {};
|
||||
panelBinding.scope = UIShortcutScope::Panel;
|
||||
panelBinding.ownerId = 20u;
|
||||
panelBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
panelBinding.chord.modifiers.control = true;
|
||||
panelBinding.commandId = "panel.command";
|
||||
registry.RegisterBinding(panelBinding);
|
||||
|
||||
UIShortcutContext context = {};
|
||||
context.focusedPath = { 10u, 20u, 30u };
|
||||
context.activePath = { 90u, 91u };
|
||||
context.commandScope.path = context.focusedPath;
|
||||
context.commandScope.windowId = 10u;
|
||||
context.commandScope.panelId = 20u;
|
||||
context.commandScope.widgetId = 30u;
|
||||
|
||||
const auto match = registry.Match(MakeCtrlPEvent(), context);
|
||||
ASSERT_TRUE(match.matched);
|
||||
EXPECT_EQ(match.binding.commandId, "panel.command");
|
||||
}
|
||||
|
||||
TEST(UIShortcutScopeTest, RegistryPrefersPanelThenWindowThenGlobalWithinFocusedCommandScope) {
|
||||
UIShortcutRegistry registry = {};
|
||||
|
||||
UIShortcutBinding globalBinding = {};
|
||||
globalBinding.scope = UIShortcutScope::Global;
|
||||
globalBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
globalBinding.chord.modifiers.control = true;
|
||||
globalBinding.commandId = "global.command";
|
||||
registry.RegisterBinding(globalBinding);
|
||||
|
||||
UIShortcutBinding windowBinding = {};
|
||||
windowBinding.scope = UIShortcutScope::Window;
|
||||
windowBinding.ownerId = 10u;
|
||||
windowBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
windowBinding.chord.modifiers.control = true;
|
||||
windowBinding.commandId = "window.command";
|
||||
registry.RegisterBinding(windowBinding);
|
||||
|
||||
UIShortcutBinding panelBinding = {};
|
||||
panelBinding.scope = UIShortcutScope::Panel;
|
||||
panelBinding.ownerId = 20u;
|
||||
panelBinding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
panelBinding.chord.modifiers.control = true;
|
||||
panelBinding.commandId = "panel.command";
|
||||
registry.RegisterBinding(panelBinding);
|
||||
|
||||
UIShortcutContext context = {};
|
||||
context.focusedPath = { 10u, 20u, 30u };
|
||||
context.commandScope.path = context.focusedPath;
|
||||
context.commandScope.windowId = 10u;
|
||||
context.commandScope.panelId = 20u;
|
||||
context.commandScope.widgetId = 30u;
|
||||
|
||||
const auto match = registry.Match(MakeCtrlPEvent(), context);
|
||||
ASSERT_TRUE(match.matched);
|
||||
EXPECT_EQ(match.binding.commandId, "panel.command");
|
||||
}
|
||||
|
||||
TEST(UIShortcutScopeTest, InputDispatcherConsumesShortcutBeforeRoutingWhenCommandScopeMatches) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
dispatcher.GetFocusController().SetFocusedPath({ 10u, 20u, 30u });
|
||||
|
||||
UIShortcutContext shortcutContext = {};
|
||||
shortcutContext.commandScope.path = { 10u, 20u, 30u };
|
||||
shortcutContext.commandScope.windowId = 10u;
|
||||
shortcutContext.commandScope.panelId = 20u;
|
||||
shortcutContext.commandScope.widgetId = 30u;
|
||||
dispatcher.SetShortcutContext(shortcutContext);
|
||||
|
||||
UIShortcutBinding binding = {};
|
||||
binding.scope = UIShortcutScope::Panel;
|
||||
binding.ownerId = 20u;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
binding.chord.modifiers.control = true;
|
||||
binding.commandId = "panel.command";
|
||||
dispatcher.GetShortcutRegistry().RegisterBinding(binding);
|
||||
|
||||
bool handlerCalled = false;
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakeCtrlPEvent(),
|
||||
{},
|
||||
[&handlerCalled](const auto&) {
|
||||
handlerCalled = true;
|
||||
return UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_TRUE(summary.shortcutMatched);
|
||||
EXPECT_TRUE(summary.shortcutHandled);
|
||||
EXPECT_FALSE(summary.shortcutSuppressed);
|
||||
EXPECT_EQ(summary.commandId, "panel.command");
|
||||
EXPECT_EQ(summary.shortcutScope, UIShortcutScope::Panel);
|
||||
EXPECT_EQ(summary.shortcutOwnerId, 20u);
|
||||
EXPECT_FALSE(handlerCalled);
|
||||
}
|
||||
|
||||
TEST(UIShortcutScopeTest, InputDispatcherSuppressesShortcutWhileTextInputIsActiveButStillRoutesEvent) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
dispatcher.GetFocusController().SetFocusedPath({ 10u, 20u, 30u });
|
||||
|
||||
UIShortcutContext shortcutContext = {};
|
||||
shortcutContext.commandScope.path = { 10u, 20u, 30u };
|
||||
shortcutContext.commandScope.windowId = 10u;
|
||||
shortcutContext.commandScope.panelId = 20u;
|
||||
shortcutContext.commandScope.widgetId = 30u;
|
||||
shortcutContext.textInputActive = true;
|
||||
dispatcher.SetShortcutContext(shortcutContext);
|
||||
|
||||
UIShortcutBinding binding = {};
|
||||
binding.scope = UIShortcutScope::Panel;
|
||||
binding.ownerId = 20u;
|
||||
binding.chord.keyCode = static_cast<std::int32_t>(KeyCode::P);
|
||||
binding.chord.modifiers.control = true;
|
||||
binding.commandId = "panel.command";
|
||||
dispatcher.GetShortcutRegistry().RegisterBinding(binding);
|
||||
|
||||
bool handlerCalled = false;
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakeCtrlPEvent(),
|
||||
{},
|
||||
[&handlerCalled](const auto&) {
|
||||
handlerCalled = true;
|
||||
return UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_TRUE(summary.shortcutMatched);
|
||||
EXPECT_FALSE(summary.shortcutHandled);
|
||||
EXPECT_TRUE(summary.shortcutSuppressed);
|
||||
EXPECT_EQ(summary.commandId, "panel.command");
|
||||
EXPECT_TRUE(handlerCalled);
|
||||
}
|
||||
112
tests/UI/Core/unit/test_ui_splitter_interaction.cpp
Normal file
112
tests/UI/Core/unit/test_ui_splitter_interaction.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Layout/UISplitterLayout.h>
|
||||
#include <XCEngine/UI/Widgets/UISplitterInteraction.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Layout::UILayoutAxis;
|
||||
using XCEngine::UI::Layout::ArrangeUISplitter;
|
||||
using XCEngine::UI::Layout::UISplitterConstraints;
|
||||
using XCEngine::UI::Layout::UISplitterLayoutResult;
|
||||
using XCEngine::UI::Layout::UISplitterMetrics;
|
||||
using XCEngine::UI::Widgets::BeginUISplitterDrag;
|
||||
using XCEngine::UI::Widgets::EndUISplitterDrag;
|
||||
using XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect;
|
||||
using XCEngine::UI::Widgets::HitTestUISplitterHandle;
|
||||
using XCEngine::UI::Widgets::UISplitterDragState;
|
||||
using XCEngine::UI::Widgets::UpdateUISplitterDrag;
|
||||
|
||||
void ExpectRect(
|
||||
const UIRect& rect,
|
||||
float x,
|
||||
float y,
|
||||
float width,
|
||||
float height) {
|
||||
EXPECT_FLOAT_EQ(rect.x, x);
|
||||
EXPECT_FLOAT_EQ(rect.y, y);
|
||||
EXPECT_FLOAT_EQ(rect.width, width);
|
||||
EXPECT_FLOAT_EQ(rect.height, height);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UISplitterInteractionTest, HorizontalSplitterHitRectExpandsAcrossDividerWidth) {
|
||||
const UIRect hitRect = ExpandUISplitterHandleHitRect(
|
||||
UIRect(240.0f, 24.0f, 8.0f, 260.0f),
|
||||
UILayoutAxis::Horizontal,
|
||||
4.0f);
|
||||
|
||||
ExpectRect(hitRect, 236.0f, 24.0f, 16.0f, 260.0f);
|
||||
}
|
||||
|
||||
TEST(UISplitterInteractionTest, VerticalSplitterHitRectExpandsAcrossDividerHeight) {
|
||||
const UIRect hitRect = ExpandUISplitterHandleHitRect(
|
||||
UIRect(16.0f, 180.0f, 420.0f, 10.0f),
|
||||
UILayoutAxis::Vertical,
|
||||
5.0f);
|
||||
|
||||
ExpectRect(hitRect, 16.0f, 175.0f, 420.0f, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UISplitterInteractionTest, HitTestUsesExpandedHorizontalHandleRect) {
|
||||
EXPECT_TRUE(HitTestUISplitterHandle(
|
||||
UIRect(240.0f, 24.0f, 8.0f, 260.0f),
|
||||
UILayoutAxis::Horizontal,
|
||||
UIPoint(236.5f, 80.0f),
|
||||
4.0f));
|
||||
EXPECT_FALSE(HitTestUISplitterHandle(
|
||||
UIRect(240.0f, 24.0f, 8.0f, 260.0f),
|
||||
UILayoutAxis::Horizontal,
|
||||
UIPoint(235.0f, 80.0f),
|
||||
4.0f));
|
||||
}
|
||||
|
||||
TEST(UISplitterInteractionTest, HitTestUsesExpandedVerticalHandleRect) {
|
||||
EXPECT_TRUE(HitTestUISplitterHandle(
|
||||
UIRect(16.0f, 180.0f, 420.0f, 10.0f),
|
||||
UILayoutAxis::Vertical,
|
||||
UIPoint(90.0f, 176.0f),
|
||||
5.0f));
|
||||
EXPECT_FALSE(HitTestUISplitterHandle(
|
||||
UIRect(16.0f, 180.0f, 420.0f, 10.0f),
|
||||
UILayoutAxis::Vertical,
|
||||
UIPoint(90.0f, 174.0f),
|
||||
5.0f));
|
||||
}
|
||||
|
||||
TEST(UISplitterInteractionTest, DragUpdateClampsPrimaryExtentAgainstConstraints) {
|
||||
UISplitterConstraints constraints = {};
|
||||
constraints.primaryMin = 120.0f;
|
||||
constraints.secondaryMin = 180.0f;
|
||||
const UISplitterMetrics metrics = { 10.0f, 18.0f };
|
||||
const UIRect bounds(0.0f, 0.0f, 500.0f, 180.0f);
|
||||
const UISplitterLayoutResult initialLayout = ArrangeUISplitter(
|
||||
bounds,
|
||||
UILayoutAxis::Horizontal,
|
||||
0.5f,
|
||||
constraints,
|
||||
metrics);
|
||||
|
||||
UISplitterDragState dragState = {};
|
||||
ASSERT_TRUE(BeginUISplitterDrag(
|
||||
42u,
|
||||
UILayoutAxis::Horizontal,
|
||||
bounds,
|
||||
initialLayout,
|
||||
constraints,
|
||||
metrics,
|
||||
UIPoint(initialLayout.handleRect.x + 5.0f, 90.0f),
|
||||
dragState));
|
||||
|
||||
UISplitterLayoutResult updatedLayout = {};
|
||||
ASSERT_TRUE(UpdateUISplitterDrag(dragState, UIPoint(80.0f, 90.0f), updatedLayout));
|
||||
EXPECT_FLOAT_EQ(updatedLayout.primaryExtent, 120.0f);
|
||||
EXPECT_FLOAT_EQ(updatedLayout.secondaryExtent, 370.0f);
|
||||
|
||||
EndUISplitterDrag(dragState);
|
||||
EXPECT_FALSE(dragState.active);
|
||||
EXPECT_EQ(dragState.ownerId, 0u);
|
||||
}
|
||||
76
tests/UI/Core/unit/test_ui_splitter_layout.cpp
Normal file
76
tests/UI/Core/unit/test_ui_splitter_layout.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Layout/UISplitterLayout.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Layout::ArrangeUISplitter;
|
||||
using XCEngine::UI::Layout::UILayoutAxis;
|
||||
using XCEngine::UI::Layout::UISplitterConstraints;
|
||||
using XCEngine::UI::Layout::UISplitterMetrics;
|
||||
|
||||
void ExpectRect(
|
||||
const UIRect& rect,
|
||||
float x,
|
||||
float y,
|
||||
float width,
|
||||
float height) {
|
||||
EXPECT_FLOAT_EQ(rect.x, x);
|
||||
EXPECT_FLOAT_EQ(rect.y, y);
|
||||
EXPECT_FLOAT_EQ(rect.width, width);
|
||||
EXPECT_FLOAT_EQ(rect.height, height);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UISplitterLayoutTest, ArrangeHorizontalSplitterClampsPrimaryExtentToMinimum) {
|
||||
UISplitterConstraints constraints = {};
|
||||
constraints.primaryMin = 200.0f;
|
||||
constraints.secondaryMin = 120.0f;
|
||||
|
||||
const auto result = ArrangeUISplitter(
|
||||
UIRect(0.0f, 0.0f, 600.0f, 300.0f),
|
||||
UILayoutAxis::Horizontal,
|
||||
0.1f,
|
||||
constraints,
|
||||
UISplitterMetrics{ 10.0f, 18.0f });
|
||||
|
||||
ExpectRect(result.primaryRect, 0.0f, 0.0f, 200.0f, 300.0f);
|
||||
ExpectRect(result.handleRect, 200.0f, 0.0f, 10.0f, 300.0f);
|
||||
ExpectRect(result.secondaryRect, 210.0f, 0.0f, 390.0f, 300.0f);
|
||||
EXPECT_NEAR(result.splitRatio, 200.0f / 590.0f, 0.0001f);
|
||||
}
|
||||
|
||||
TEST(UISplitterLayoutTest, ArrangeVerticalSplitterClampsSecondaryMinimumAgainstRequestedRatio) {
|
||||
UISplitterConstraints constraints = {};
|
||||
constraints.primaryMin = 100.0f;
|
||||
constraints.secondaryMin = 120.0f;
|
||||
|
||||
const auto result = ArrangeUISplitter(
|
||||
UIRect(0.0f, 0.0f, 500.0f, 400.0f),
|
||||
UILayoutAxis::Vertical,
|
||||
0.9f,
|
||||
constraints,
|
||||
UISplitterMetrics{ 8.0f, 16.0f });
|
||||
|
||||
ExpectRect(result.primaryRect, 0.0f, 0.0f, 500.0f, 272.0f);
|
||||
ExpectRect(result.handleRect, 0.0f, 272.0f, 500.0f, 8.0f);
|
||||
ExpectRect(result.secondaryRect, 0.0f, 280.0f, 500.0f, 120.0f);
|
||||
EXPECT_NEAR(result.splitRatio, 272.0f / 392.0f, 0.0001f);
|
||||
}
|
||||
|
||||
TEST(UISplitterLayoutTest, HorizontalArrangementSplitsAvailableExtentAroundHandle) {
|
||||
const auto result = ArrangeUISplitter(
|
||||
UIRect(10.0f, 20.0f, 400.0f, 120.0f),
|
||||
UILayoutAxis::Horizontal,
|
||||
0.5f,
|
||||
{},
|
||||
UISplitterMetrics{ 8.0f, 14.0f });
|
||||
|
||||
EXPECT_FLOAT_EQ(result.primaryExtent, 196.0f);
|
||||
EXPECT_FLOAT_EQ(result.secondaryExtent, 196.0f);
|
||||
ExpectRect(result.primaryRect, 10.0f, 20.0f, 196.0f, 120.0f);
|
||||
ExpectRect(result.handleRect, 206.0f, 20.0f, 8.0f, 120.0f);
|
||||
ExpectRect(result.secondaryRect, 214.0f, 20.0f, 196.0f, 120.0f);
|
||||
}
|
||||
Reference in New Issue
Block a user