#include "XCUIBackend/XCUIEditorCommandRouter.h" #include #include namespace { using XCEngine::Editor::XCUIBackend::XCUIEditorCommandAccelerator; using XCEngine::Editor::XCUIBackend::XCUIEditorCommandDefinition; using XCEngine::Editor::XCUIBackend::XCUIEditorCommandInputSnapshot; using XCEngine::Editor::XCUIBackend::XCUIEditorCommandKeyState; using XCEngine::Editor::XCUIBackend::XCUIEditorCommandRouter; using XCEngine::Editor::XCUIBackend::XCUIEditorCommandShortcutMatch; using XCEngine::Editor::XCUIBackend::XCUIEditorCommandShortcutQuery; using XCEngine::Input::KeyCode; XCUIEditorCommandKeyState MakeKeyState(std::int32_t keyCode, bool down, bool repeat = false) { XCUIEditorCommandKeyState state = {}; state.keyCode = keyCode; state.down = down; state.repeat = repeat; return state; } XCUIEditorCommandInputSnapshot MakeSnapshot( std::initializer_list keys, const XCEngine::UI::UIInputModifiers& modifiers = {}, bool windowFocused = true) { XCUIEditorCommandInputSnapshot snapshot = {}; snapshot.keys.assign(keys.begin(), keys.end()); snapshot.modifiers = modifiers; snapshot.windowFocused = windowFocused; return snapshot; } TEST(XCUIEditorCommandRouterTest, RegisterInvokeAndUnregisterTrackCommandsById) { XCUIEditorCommandRouter router = {}; int invokeCount = 0; XCUIEditorCommandDefinition definition = {}; definition.commandId = "xcui.file.save"; definition.invoke = [&invokeCount]() { ++invokeCount; }; EXPECT_TRUE(router.RegisterCommand(definition)); EXPECT_EQ(router.GetCommandCount(), 1u); EXPECT_TRUE(router.HasCommand("xcui.file.save")); EXPECT_TRUE(router.IsCommandEnabled("xcui.file.save")); EXPECT_TRUE(router.InvokeCommand("xcui.file.save")); EXPECT_EQ(invokeCount, 1); EXPECT_TRUE(router.UnregisterCommand("xcui.file.save")); EXPECT_FALSE(router.HasCommand("xcui.file.save")); EXPECT_FALSE(router.InvokeCommand("xcui.file.save")); EXPECT_EQ(router.GetCommandCount(), 0u); } TEST(XCUIEditorCommandRouterTest, RegisterRejectsMissingIdOrHandlerAndDuplicateIdReplacesEntry) { XCUIEditorCommandRouter router = {}; int invokeCount = 0; XCUIEditorCommandDefinition invalid = {}; invalid.commandId = "xcui.invalid"; EXPECT_FALSE(router.RegisterCommand(invalid)); invalid = {}; invalid.invoke = []() {}; EXPECT_FALSE(router.RegisterCommand(invalid)); XCUIEditorCommandDefinition first = {}; first.commandId = "xcui.edit.rename"; first.invoke = [&invokeCount]() { invokeCount += 1; }; first.accelerators.push_back({ static_cast(KeyCode::F2), {}, true, false }); ASSERT_TRUE(router.RegisterCommand(first)); XCUIEditorCommandDefinition replacement = {}; replacement.commandId = "xcui.edit.rename"; replacement.invoke = [&invokeCount]() { invokeCount += 10; }; replacement.accelerators.push_back({ static_cast(KeyCode::Enter), {}, true, false }); ASSERT_TRUE(router.RegisterCommand(replacement)); EXPECT_EQ(router.GetCommandCount(), 1u); EXPECT_TRUE(router.InvokeCommand("xcui.edit.rename")); EXPECT_EQ(invokeCount, 10); const XCUIEditorCommandInputSnapshot f2Snapshot = MakeSnapshot({ MakeKeyState(static_cast(KeyCode::F2), true) }); const auto f2Match = router.MatchShortcut({ &f2Snapshot }); EXPECT_FALSE(f2Match.matched); const XCUIEditorCommandInputSnapshot enterSnapshot = MakeSnapshot({ MakeKeyState(static_cast(KeyCode::Enter), true) }); const auto enterMatch = router.MatchShortcut({ &enterSnapshot }); EXPECT_TRUE(enterMatch.matched); EXPECT_EQ(enterMatch.commandId, "xcui.edit.rename"); } TEST(XCUIEditorCommandRouterTest, DisabledPredicateBlocksDirectAndShortcutInvocation) { XCUIEditorCommandRouter router = {}; bool enabled = false; int invokeCount = 0; XCUIEditorCommandDefinition definition = {}; definition.commandId = "xcui.edit.delete"; definition.invoke = [&invokeCount]() { ++invokeCount; }; definition.isEnabled = [&enabled]() { return enabled; }; definition.accelerators.push_back({ static_cast(KeyCode::Delete), {}, true, false }); ASSERT_TRUE(router.RegisterCommand(definition)); const XCUIEditorCommandInputSnapshot snapshot = MakeSnapshot({ MakeKeyState(static_cast(KeyCode::Delete), true) }); EXPECT_FALSE(router.IsCommandEnabled("xcui.edit.delete")); EXPECT_FALSE(router.InvokeCommand("xcui.edit.delete")); EXPECT_FALSE(router.MatchShortcut({ &snapshot }).matched); EXPECT_FALSE(router.InvokeMatchingShortcut({ &snapshot })); EXPECT_EQ(invokeCount, 0); enabled = true; EXPECT_TRUE(router.IsCommandEnabled("xcui.edit.delete")); EXPECT_TRUE(router.InvokeMatchingShortcut({ &snapshot })); EXPECT_EQ(invokeCount, 1); } TEST(XCUIEditorCommandRouterTest, ShortcutMatchingRespectsModifiersRepeatAndPolicyFlags) { XCUIEditorCommandRouter router = {}; int invokeCount = 0; XCEngine::UI::UIInputModifiers modifiers = {}; modifiers.control = true; modifiers.shift = true; XCUIEditorCommandDefinition definition = {}; definition.commandId = "xcui.search.command_palette"; definition.invoke = [&invokeCount]() { ++invokeCount; }; definition.accelerators.push_back({ static_cast(KeyCode::P), modifiers, true, false }); ASSERT_TRUE(router.RegisterCommand(definition)); const XCUIEditorCommandInputSnapshot exactSnapshot = MakeSnapshot( { MakeKeyState(static_cast(KeyCode::P), true) }, modifiers); auto match = router.MatchShortcut({ &exactSnapshot }); ASSERT_TRUE(match.matched); EXPECT_EQ(match.commandId, "xcui.search.command_palette"); const XCUIEditorCommandInputSnapshot repeatedSnapshot = MakeSnapshot( { MakeKeyState(static_cast(KeyCode::P), true, true) }, modifiers); EXPECT_FALSE(router.MatchShortcut({ &repeatedSnapshot }).matched); XCUIEditorCommandInputSnapshot captureSnapshot = exactSnapshot; captureSnapshot.wantCaptureKeyboard = true; EXPECT_FALSE(router.MatchShortcut({ &captureSnapshot }).matched); EXPECT_TRUE(router.MatchShortcut({ &captureSnapshot, true, true, false }).matched); XCUIEditorCommandInputSnapshot textInputSnapshot = exactSnapshot; textInputSnapshot.wantTextInput = true; EXPECT_FALSE(router.MatchShortcut({ &textInputSnapshot }).matched); EXPECT_TRUE(router.MatchShortcut({ &textInputSnapshot, true, false, true }).matched); XCUIEditorCommandInputSnapshot unfocusedSnapshot = exactSnapshot; unfocusedSnapshot.windowFocused = false; EXPECT_FALSE(router.MatchShortcut({ &unfocusedSnapshot }).matched); EXPECT_TRUE(router.MatchShortcut({ &unfocusedSnapshot, false, false, false }).matched); XCUIEditorCommandShortcutMatch invokedMatch = {}; EXPECT_TRUE(router.InvokeMatchingShortcut({ &exactSnapshot }, &invokedMatch)); EXPECT_TRUE(invokedMatch.matched); EXPECT_EQ(invokedMatch.commandId, "xcui.search.command_palette"); EXPECT_EQ(invokeCount, 1); } TEST(XCUIEditorCommandRouterTest, NonExactModifierAcceleratorsAllowAdditionalPressedModifiers) { XCUIEditorCommandRouter router = {}; XCEngine::UI::UIInputModifiers requiredModifiers = {}; requiredModifiers.control = true; XCUIEditorCommandDefinition definition = {}; definition.commandId = "xcui.edit.duplicate"; definition.invoke = []() {}; definition.accelerators.push_back({ static_cast(KeyCode::D), requiredModifiers, false, false }); ASSERT_TRUE(router.RegisterCommand(definition)); XCEngine::UI::UIInputModifiers actualModifiers = requiredModifiers; actualModifiers.shift = true; const XCUIEditorCommandInputSnapshot permissiveSnapshot = MakeSnapshot( { MakeKeyState(static_cast(KeyCode::D), true) }, actualModifiers); EXPECT_TRUE(router.MatchShortcut({ &permissiveSnapshot }).matched); XCUIEditorCommandRouter exactRouter = {}; definition.accelerators.front().exactModifiers = true; ASSERT_TRUE(exactRouter.RegisterCommand(definition)); EXPECT_FALSE(exactRouter.MatchShortcut({ &permissiveSnapshot }).matched); } } // namespace