Add XCUI input state validation sandbox batch
This commit is contained in:
@@ -8,6 +8,7 @@ set(UI_TEST_SOURCES
|
||||
test_ui_editor_panel_chrome.cpp
|
||||
test_ui_expansion_model.cpp
|
||||
test_ui_flat_hierarchy_helpers.cpp
|
||||
test_ui_input_dispatcher.cpp
|
||||
test_ui_keyboard_navigation_model.cpp
|
||||
test_ui_property_edit_model.cpp
|
||||
test_layout_engine.cpp
|
||||
|
||||
110
tests/Core/UI/test_ui_input_dispatcher.cpp
Normal file
110
tests/Core/UI/test_ui_input_dispatcher.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputDispatchRequest;
|
||||
using XCEngine::UI::UIInputDispatcher;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIInputPath;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerDownTransfersFocusAndStartsActivePath) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath hoveredPath = { 10u, 20u, 30u };
|
||||
std::vector<UIInputDispatchRequest> routedRequests = {};
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, UIPointerButton::Left),
|
||||
hoveredPath,
|
||||
[&](const UIInputDispatchRequest& request) {
|
||||
routedRequests.push_back(request);
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_TRUE(summary.focusChange.Changed());
|
||||
EXPECT_EQ(summary.focusChange.previousPath, UIInputPath());
|
||||
EXPECT_EQ(summary.focusChange.currentPath, hoveredPath);
|
||||
EXPECT_EQ(dispatcher.GetFocusController().GetFocusedPath(), hoveredPath);
|
||||
EXPECT_EQ(dispatcher.GetFocusController().GetActivePath(), hoveredPath);
|
||||
ASSERT_FALSE(routedRequests.empty());
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, hoveredPath);
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Hovered);
|
||||
const auto targetIt = std::find_if(
|
||||
routedRequests.begin(),
|
||||
routedRequests.end(),
|
||||
[](const UIInputDispatchRequest& request) {
|
||||
return request.isTargetElement;
|
||||
});
|
||||
ASSERT_NE(targetIt, routedRequests.end());
|
||||
EXPECT_EQ(targetIt->elementId, hoveredPath.Target());
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerCaptureOverridesHoveredRouteForPointerEvents) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath hoveredPath = { 41u, 42u };
|
||||
const UIInputPath capturePath = { 7u, 8u, 9u };
|
||||
dispatcher.GetFocusController().SetPointerCapturePath(capturePath);
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerMove),
|
||||
hoveredPath,
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Captured);
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, capturePath);
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerButtonUpClearsActivePathAfterDispatch) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath activePath = { 2u, 4u, 6u };
|
||||
dispatcher.GetFocusController().SetActivePath(activePath);
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerButtonUp, UIPointerButton::Left),
|
||||
{},
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_FALSE(summary.routing.handled);
|
||||
EXPECT_FALSE(dispatcher.GetFocusController().HasActivePath());
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, KeyboardEventsRouteToFocusedPath) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath focusedPath = { 101u, 202u };
|
||||
dispatcher.GetFocusController().SetFocusedPath(focusedPath);
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = 'F';
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
event,
|
||||
{},
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Focused);
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, focusedPath);
|
||||
}
|
||||
@@ -153,6 +153,21 @@ bool TryFindSmallestFilledRectContainingPoint(
|
||||
return found;
|
||||
}
|
||||
|
||||
bool TryFindFilledRectForText(
|
||||
const XCEngine::UI::UIDrawData& drawData,
|
||||
const std::string& text,
|
||||
XCEngine::UI::UIRect& outRect) {
|
||||
const auto* textCommand = FindTextCommand(drawData, text);
|
||||
return textCommand != nullptr &&
|
||||
TryFindSmallestFilledRectContainingPoint(drawData, textCommand->position, outRect);
|
||||
}
|
||||
|
||||
XCEngine::UI::UIPoint GetRectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
return XCEngine::UI::UIPoint(
|
||||
rect.x + rect.width * 0.5f,
|
||||
rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
std::size_t CountCommandsOfType(
|
||||
const XCEngine::UI::UIDrawData& drawData,
|
||||
XCEngine::UI::UIDrawCommandType type) {
|
||||
@@ -384,6 +399,122 @@ TEST(UIRuntimeTest, DocumentHostScrollViewClipsOverflowingChildrenAndRespondsToW
|
||||
EXPECT_FLOAT_EQ(line01Persisted->position.y, line01AfterY);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, DocumentHostTracksHoverFocusAndPointerCaptureAcrossFrames) {
|
||||
TempFileScope viewFile(
|
||||
"xcui_runtime_input_states",
|
||||
".xcui",
|
||||
"<View name=\"Input State Test\">\n"
|
||||
" <Column padding=\"18\" gap=\"10\">\n"
|
||||
" <Button id=\"input-hover\" text=\"Hover / Focus\" />\n"
|
||||
" <Button id=\"input-capture\" text=\"Pointer Capture\" capturePointer=\"true\" />\n"
|
||||
" <Button id=\"input-route\" text=\"Route Target\" />\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.input.states")));
|
||||
|
||||
UIScreenFrameInput firstInput = BuildInputState(1u);
|
||||
firstInput.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 520.0f, 260.0f);
|
||||
const auto& firstFrame = player.Update(firstInput);
|
||||
const auto* hoverButtonText = FindTextCommand(firstFrame.drawData, "Hover / Focus");
|
||||
const auto* captureButtonText = FindTextCommand(firstFrame.drawData, "Pointer Capture");
|
||||
const auto* routeButtonText = FindTextCommand(firstFrame.drawData, "Route Target");
|
||||
ASSERT_NE(hoverButtonText, nullptr);
|
||||
ASSERT_NE(captureButtonText, nullptr);
|
||||
ASSERT_NE(routeButtonText, nullptr);
|
||||
|
||||
XCEngine::UI::UIRect hoverButtonRect = {};
|
||||
XCEngine::UI::UIRect captureButtonRect = {};
|
||||
XCEngine::UI::UIRect routeButtonRect = {};
|
||||
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Hover / Focus", hoverButtonRect));
|
||||
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Pointer Capture", captureButtonRect));
|
||||
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Route Target", routeButtonRect));
|
||||
|
||||
const XCEngine::UI::UIPoint hoverButtonPoint = GetRectCenter(hoverButtonRect);
|
||||
const XCEngine::UI::UIPoint captureButtonPoint = GetRectCenter(captureButtonRect);
|
||||
const XCEngine::UI::UIPoint routeButtonPoint = GetRectCenter(routeButtonRect);
|
||||
|
||||
SCOPED_TRACE(
|
||||
std::string("hoverPos=") + std::to_string(hoverButtonText->position.x) + "," + std::to_string(hoverButtonText->position.y) +
|
||||
" hoverRect=" + std::to_string(hoverButtonRect.x) + "," + std::to_string(hoverButtonRect.y) + "," +
|
||||
std::to_string(hoverButtonRect.width) + "," + std::to_string(hoverButtonRect.height) +
|
||||
" capturePos=" + std::to_string(captureButtonText->position.x) + "," + std::to_string(captureButtonText->position.y) +
|
||||
" captureRect=" + std::to_string(captureButtonRect.x) + "," + std::to_string(captureButtonRect.y) + "," +
|
||||
std::to_string(captureButtonRect.width) + "," + std::to_string(captureButtonRect.height) +
|
||||
" routePos=" + std::to_string(routeButtonText->position.x) + "," + std::to_string(routeButtonText->position.y) +
|
||||
" routeRect=" + std::to_string(routeButtonRect.x) + "," + std::to_string(routeButtonRect.y) + "," +
|
||||
std::to_string(routeButtonRect.width) + "," + std::to_string(routeButtonRect.height));
|
||||
|
||||
UIScreenFrameInput hoverInput = BuildInputState(2u);
|
||||
hoverInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent hoverEvent = {};
|
||||
hoverEvent.type = XCEngine::UI::UIInputEventType::PointerMove;
|
||||
hoverEvent.position = hoverButtonPoint;
|
||||
hoverInput.events.push_back(hoverEvent);
|
||||
player.Update(hoverInput);
|
||||
const auto& afterHover = host.GetInputDebugSnapshot();
|
||||
EXPECT_NE(afterHover.hoveredStateKey.find("/input-hover"), std::string::npos);
|
||||
EXPECT_TRUE(afterHover.focusedStateKey.empty());
|
||||
|
||||
UIScreenFrameInput captureDownInput = BuildInputState(3u);
|
||||
captureDownInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent captureDownEvent = {};
|
||||
captureDownEvent.type = XCEngine::UI::UIInputEventType::PointerButtonDown;
|
||||
captureDownEvent.pointerButton = XCEngine::UI::UIPointerButton::Left;
|
||||
captureDownEvent.position = captureButtonPoint;
|
||||
captureDownInput.events.push_back(captureDownEvent);
|
||||
player.Update(captureDownInput);
|
||||
const auto& afterCaptureDown = host.GetInputDebugSnapshot();
|
||||
SCOPED_TRACE(
|
||||
std::string("afterCaptureDown hovered=") + afterCaptureDown.hoveredStateKey +
|
||||
" focused=" + afterCaptureDown.focusedStateKey +
|
||||
" active=" + afterCaptureDown.activeStateKey +
|
||||
" capture=" + afterCaptureDown.captureStateKey +
|
||||
" lastKind=" + afterCaptureDown.lastTargetKind +
|
||||
" lastTarget=" + afterCaptureDown.lastTargetStateKey +
|
||||
" result=" + afterCaptureDown.lastResult);
|
||||
EXPECT_NE(afterCaptureDown.focusedStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_NE(afterCaptureDown.activeStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_NE(afterCaptureDown.captureStateKey.find("/input-capture"), std::string::npos);
|
||||
|
||||
UIScreenFrameInput dragInput = BuildInputState(4u);
|
||||
dragInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent dragEvent = {};
|
||||
dragEvent.type = XCEngine::UI::UIInputEventType::PointerMove;
|
||||
dragEvent.position = routeButtonPoint;
|
||||
dragInput.events.push_back(dragEvent);
|
||||
player.Update(dragInput);
|
||||
const auto& afterDrag = host.GetInputDebugSnapshot();
|
||||
SCOPED_TRACE(
|
||||
std::string("afterDrag hovered=") + afterDrag.hoveredStateKey +
|
||||
" focused=" + afterDrag.focusedStateKey +
|
||||
" active=" + afterDrag.activeStateKey +
|
||||
" capture=" + afterDrag.captureStateKey +
|
||||
" lastKind=" + afterDrag.lastTargetKind +
|
||||
" lastTarget=" + afterDrag.lastTargetStateKey +
|
||||
" result=" + afterDrag.lastResult);
|
||||
EXPECT_NE(afterDrag.hoveredStateKey.find("/input-route"), std::string::npos);
|
||||
EXPECT_NE(afterDrag.captureStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_EQ(afterDrag.lastTargetKind, "Captured");
|
||||
EXPECT_NE(afterDrag.lastTargetStateKey.find("/input-capture"), std::string::npos);
|
||||
|
||||
UIScreenFrameInput releaseInput = BuildInputState(5u);
|
||||
releaseInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent releaseEvent = {};
|
||||
releaseEvent.type = XCEngine::UI::UIInputEventType::PointerButtonUp;
|
||||
releaseEvent.pointerButton = XCEngine::UI::UIPointerButton::Left;
|
||||
releaseEvent.position = routeButtonPoint;
|
||||
releaseInput.events.push_back(releaseEvent);
|
||||
player.Update(releaseInput);
|
||||
const auto& afterRelease = host.GetInputDebugSnapshot();
|
||||
EXPECT_TRUE(afterRelease.activeStateKey.empty());
|
||||
EXPECT_TRUE(afterRelease.captureStateKey.empty());
|
||||
EXPECT_NE(afterRelease.focusedStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_NE(afterRelease.hoveredStateKey.find("/input-route"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
|
||||
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
|
||||
UIDocumentScreenHost host = {};
|
||||
|
||||
Reference in New Issue
Block a user