Add dock host interaction contract validation

This commit is contained in:
2026-04-07 10:41:39 +08:00
parent f31fece2ce
commit ce1995659a
12 changed files with 1430 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_structured_editor_shell.cpp
test_ui_editor_command_dispatcher.cpp
test_ui_editor_command_registry.cpp
test_ui_editor_dock_host_interaction.cpp
test_ui_editor_menu_model.cpp
test_ui_editor_menu_session.cpp
test_ui_editor_menu_bar.cpp

View File

@@ -0,0 +1,302 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorDockHostInteraction.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
namespace {
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
using XCEngine::UI::Editor::UIEditorDockHostInteractionState;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::UpdateUIEditorDockHostInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", {}, true, true, true },
{ "doc-b", "Document B", {}, true, true, true },
{ "details", "Details", {}, true, true, true },
{ "console", "Console", {}, true, true, true }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.5f,
BuildUIEditorWorkspaceTabStack(
"document-tabs",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
},
1u),
BuildUIEditorWorkspaceSplit(
"right-split",
UIEditorWorkspaceSplitAxis::Vertical,
0.6f,
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true),
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true)));
workspace.activePanelId = "doc-b";
return workspace;
}
UIInputEvent MakePointerMove(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerMove;
event.position = UIPoint(x, y);
return event;
}
UIInputEvent MakePointerDown(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonDown;
event.position = UIPoint(x, y);
event.pointerButton = UIPointerButton::Left;
return event;
}
UIInputEvent MakePointerUp(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonUp;
event.position = UIPoint(x, y);
event.pointerButton = UIPointerButton::Left;
return event;
}
UIInputEvent MakeFocusLost() {
UIInputEvent event = {};
event.type = UIInputEventType::FocusLost;
return event;
}
UIPoint RectCenter(const UIRect& rect) {
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
}
} // namespace
TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {};
auto frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(396.0f, 120.0f) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::SplitterHandle);
EXPECT_EQ(frame.result.hitTarget.nodeId, "root-split");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerDown(396.0f, 120.0f) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.requestPointerCapture);
EXPECT_EQ(frame.result.activeSplitterNodeId, "root-split");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(520.0f, 120.0f) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.layoutChanged);
EXPECT_GT(controller.GetWorkspace().root.splitRatio, 0.5f);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerUp(520.0f, 120.0f) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.releasePointerCapture);
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
}
TEST(UIEditorDockHostInteractionTest, FocusLostWhileDraggingSplitterRequestsPointerRelease) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {};
auto frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(396.0f, 120.0f) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::SplitterHandle);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerDown(396.0f, 120.0f) });
EXPECT_TRUE(frame.result.requestPointerCapture);
EXPECT_FALSE(state.dockHostState.activeSplitterNodeId.empty());
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakeFocusLost() });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.releasePointerCapture);
EXPECT_TRUE(state.dockHostState.activeSplitterNodeId.empty());
}
TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {};
auto frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{});
ASSERT_EQ(frame.layout.tabStacks.size(), 1u);
const UIRect docARect = frame.layout.tabStacks.front().tabStripLayout.tabHeaderRects[0];
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(RectCenter(docARect).x, RectCenter(docARect).y) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::Tab);
EXPECT_EQ(frame.result.hitTarget.panelId, "doc-a");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerUp(RectCenter(docARect).x, RectCenter(docARect).y) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.commandExecuted);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
ASSERT_EQ(frame.layout.tabStacks.size(), 1u);
EXPECT_EQ(frame.layout.tabStacks.front().selectedPanelId, "doc-a");
}
TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughController) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {};
auto frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{});
ASSERT_EQ(frame.layout.tabStacks.size(), 1u);
const UIRect closeRect = frame.layout.tabStacks.front().tabStripLayout.closeButtonRects[1];
const UIPoint closeCenter = RectCenter(closeRect);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(closeCenter.x, closeCenter.y) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::TabCloseButton);
EXPECT_EQ(frame.result.hitTarget.panelId, "doc-b");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerUp(closeCenter.x, closeCenter.y) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.commandExecuted);
const auto* panelState = FindUIEditorPanelSessionState(controller.GetSession(), "doc-b");
ASSERT_NE(panelState, nullptr);
EXPECT_FALSE(panelState->open);
EXPECT_FALSE(panelState->visible);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a");
}
TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelBodyActivatesTargetPanel) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {};
auto frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{});
ASSERT_EQ(frame.layout.panels.size(), 2u);
const UIRect detailsBodyRect = frame.layout.panels[0].frameLayout.bodyRect;
const UIPoint detailsBodyCenter = RectCenter(detailsBodyRect);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(detailsBodyCenter.x, detailsBodyCenter.y) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::PanelBody);
EXPECT_EQ(frame.result.hitTarget.panelId, "details");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerUp(detailsBodyCenter.x, detailsBodyCenter.y) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.commandExecuted);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
}
TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelCloseClosesPanelThroughController) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
UIEditorDockHostInteractionState state = {};
auto frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{});
ASSERT_EQ(frame.layout.panels.size(), 2u);
const UIRect closeRect = frame.layout.panels[1].frameLayout.closeButtonRect;
const UIPoint closeCenter = RectCenter(closeRect);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(closeCenter.x, closeCenter.y) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::PanelCloseButton);
EXPECT_EQ(frame.result.hitTarget.panelId, "console");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerUp(closeCenter.x, closeCenter.y) });
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.commandExecuted);
const auto* panelState = FindUIEditorPanelSessionState(controller.GetSession(), "console");
ASSERT_NE(panelState, nullptr);
EXPECT_FALSE(panelState->open);
EXPECT_FALSE(panelState->visible);
}