From 071ea1dc975e6bd4c1072fa01ef4cdb589b8fdb5 Mon Sep 17 00:00:00 2001 From: Arjun Balgovind <32061677+arjunbalgovind@users.noreply.github.com> Date: Thu, 11 Jun 2020 13:07:46 -0700 Subject: [PATCH] KBM - Set up tests for keyboard hook remapping logic (#4004) * Add test proj, refactor proj with filters, and move single remap function to a separate header * Moved all methods to header files * remove more unused commented code * Reverted sln file * Fixed sln file * Added interface wrapping SendInput calls * fixed formatting * Created test mock class * Added keyboard input logic * Fixed compilation errors and added nuget reference to CppWinRT * Added tests for single key remapping * Refactored code for adding shortcut remap tests * Separated test classes * Fixed tests in release mode * Added more tests * Resolved comments --- PowerToys.sln | 8 +- .../keyboardmanager/common/InputInterface.h | 13 + .../common/KeyboardManagerCommon.vcxproj | 13 + .../KeyboardManagerCommon.vcxproj.filters | 6 + .../common/KeyboardManagerConstants.h | 1 + .../common/KeyboardManagerState.h | 2 +- .../keyboardmanager/common/Shortcut.cpp | 44 +- src/modules/keyboardmanager/common/Shortcut.h | 5 +- .../keyboardmanager/common/packages.config | 4 + src/modules/keyboardmanager/dll/Input.cpp | 14 + src/modules/keyboardmanager/dll/Input.h | 14 + .../dll/KeyboardEventHandlers.cpp | 42 +- .../dll/KeyboardEventHandlers.h | 13 +- .../dll/KeyboardManager.vcxproj | 14 + .../dll/KeyboardManager.vcxproj.filters | 9 + src/modules/keyboardmanager/dll/dllmain.cpp | 14 +- .../keyboardmanager/dll/packages.config | 4 + .../test/KeyboardManagerRemapLogicTest.cpp | 1310 +++++++++++++++++ .../test/KeyboardManagerTest.vcxproj | 140 ++ .../test/KeyboardManagerTest.vcxproj.filters | 45 + .../keyboardmanager/test/MockedInput.cpp | 130 ++ .../keyboardmanager/test/MockedInput.h | 38 + .../keyboardmanager/test/TestHelpers.cpp | 14 + .../keyboardmanager/test/TestHelpers.h | 9 + .../keyboardmanager/test/packages.config | 4 + src/modules/keyboardmanager/test/pch.cpp | 5 + src/modules/keyboardmanager/test/pch.h | 1 + .../ui/KeyboardManagerUI.vcxproj | 8 +- .../ui/KeyboardManagerUI.vcxproj.filters | 6 +- .../keyboardmanager/ui/packages.config | 2 +- 30 files changed, 1866 insertions(+), 66 deletions(-) create mode 100644 src/modules/keyboardmanager/common/InputInterface.h create mode 100644 src/modules/keyboardmanager/common/packages.config create mode 100644 src/modules/keyboardmanager/dll/Input.cpp create mode 100644 src/modules/keyboardmanager/dll/Input.h create mode 100644 src/modules/keyboardmanager/dll/packages.config create mode 100644 src/modules/keyboardmanager/test/KeyboardManagerRemapLogicTest.cpp create mode 100644 src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj create mode 100644 src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj.filters create mode 100644 src/modules/keyboardmanager/test/MockedInput.cpp create mode 100644 src/modules/keyboardmanager/test/MockedInput.h create mode 100644 src/modules/keyboardmanager/test/TestHelpers.cpp create mode 100644 src/modules/keyboardmanager/test/TestHelpers.h create mode 100644 src/modules/keyboardmanager/test/packages.config create mode 100644 src/modules/keyboardmanager/test/pch.cpp create mode 100644 src/modules/keyboardmanager/test/pch.h diff --git a/PowerToys.sln b/PowerToys.sln index 722bc3f73e..d2b543802d 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -199,7 +199,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Launcher", "src\m EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerLauncher", "src\modules\launcher\PowerLauncher\PowerLauncher.csproj", "{F97E5003-F263-4D4A-A964-0F1F3C82DEF2}" EndProject -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E775CC2C-24CB-48D6-9C3A-BE4CCE0DB17A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "win-app-driver", "src\tests\win-app-driver\win-app-driver.csproj", "{880ED251-9E16-4713-9A70-D35FE0C01669}" @@ -252,6 +251,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Telemetry", "src\common\Man EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "os-detection", "src\common\os-detection\os-detection.vcxproj", "{E6410BFC-B341-498C-8C67-312C20CDD8D5}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerTest", "src\modules\keyboardmanager\test\KeyboardManagerTest.vcxproj", "{62173D9A-6724-4C00-A1C8-FB646480A9EC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -490,6 +491,10 @@ Global {E6410BFC-B341-498C-8C67-312C20CDD8D5}.Debug|x64.Build.0 = Debug|x64 {E6410BFC-B341-498C-8C67-312C20CDD8D5}.Release|x64.ActiveCfg = Release|x64 {E6410BFC-B341-498C-8C67-312C20CDD8D5}.Release|x64.Build.0 = Release|x64 + {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x64.ActiveCfg = Debug|x64 + {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x64.Build.0 = Debug|x64 + {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.ActiveCfg = Release|x64 + {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -558,6 +563,7 @@ Global {08C8C05F-0362-41BC-818C-724572DF8B06} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} {5D00D290-4016-4CFE-9E41-1E7C724509BA} = {1AFB6476-670D-4E80-A464-657E01DFF482} {E6410BFC-B341-498C-8C67-312C20CDD8D5} = {1AFB6476-670D-4E80-A464-657E01DFF482} + {62173D9A-6724-4C00-A1C8-FB646480A9EC} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/keyboardmanager/common/InputInterface.h b/src/modules/keyboardmanager/common/InputInterface.h new file mode 100644 index 0000000000..4b8905f06a --- /dev/null +++ b/src/modules/keyboardmanager/common/InputInterface.h @@ -0,0 +1,13 @@ +#pragma once +#include "windows.h" + +// Interface used to wrap keyboard input library methods +class InputInterface +{ +public: + // Function to simulate input + virtual UINT SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize) = 0; + + // Function to get the state of a particular key + virtual bool GetVirtualKeyState(int key) = 0; +}; diff --git a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj index d55575bf6e..f81b9996a1 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj +++ b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj @@ -1,5 +1,6 @@ + Debug @@ -110,6 +111,7 @@ + @@ -124,7 +126,18 @@ {74485049-c722-400f-abe5-86ac52d929b3} + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters index 2e5f17c3f6..2732641e35 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters +++ b/src/modules/keyboardmanager/common/KeyboardManagerCommon.vcxproj.filters @@ -62,5 +62,11 @@ Header Files + + Header Files + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/common/KeyboardManagerConstants.h b/src/modules/keyboardmanager/common/KeyboardManagerConstants.h index 8b54f5cf0b..e2b4f532ef 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerConstants.h +++ b/src/modules/keyboardmanager/common/KeyboardManagerConstants.h @@ -1,5 +1,6 @@ #pragma once #include +#include namespace KeyboardManagerConstants { diff --git a/src/modules/keyboardmanager/common/KeyboardManagerState.h b/src/modules/keyboardmanager/common/KeyboardManagerState.h index 4c596a4d8a..97d60381c7 100644 --- a/src/modules/keyboardmanager/common/KeyboardManagerState.h +++ b/src/modules/keyboardmanager/common/KeyboardManagerState.h @@ -184,4 +184,4 @@ public: // Gets the Current Active Configuration Name. std::wstring GetCurrentConfigName(); -}; +}; diff --git a/src/modules/keyboardmanager/common/Shortcut.cpp b/src/modules/keyboardmanager/common/Shortcut.cpp index f0710a082d..4b73191005 100644 --- a/src/modules/keyboardmanager/common/Shortcut.cpp +++ b/src/modules/keyboardmanager/common/Shortcut.cpp @@ -260,7 +260,7 @@ bool Shortcut::CheckShiftKey(const DWORD& input) const // Function to set a key in the shortcut based on the passed key code argument. Returns false if it is already set to the same value. This can be used to avoid UI refreshing bool Shortcut::SetKey(const DWORD& input) { - // Since there isn't a key for a common Win key this is handled with a separate argument + // Since there isn't a key for a common Win key we use the key code defined by us if (input == CommonSharedConstants::VK_WIN_BOTH) { if (winKey == ModifierKey::Both) @@ -493,27 +493,27 @@ void Shortcut::SetKeyCodes(const std::vector& keys) } // Function to check if all the modifiers in the shortcut have been pressed down -bool Shortcut::CheckModifiersKeyboardState() const +bool Shortcut::CheckModifiersKeyboardState(InputInterface& ii) const { // Check the win key state if (winKey == ModifierKey::Both) { // Since VK_WIN does not exist, we check both VK_LWIN and VK_RWIN - if ((!(GetAsyncKeyState(VK_LWIN) & 0x8000)) && (!(GetAsyncKeyState(VK_RWIN) & 0x8000))) + if ((!(ii.GetVirtualKeyState(VK_LWIN))) && (!(ii.GetVirtualKeyState(VK_RWIN)))) { return false; } } else if (winKey == ModifierKey::Left) { - if (!(GetAsyncKeyState(VK_LWIN) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_LWIN))) { return false; } } else if (winKey == ModifierKey::Right) { - if (!(GetAsyncKeyState(VK_RWIN) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_RWIN))) { return false; } @@ -522,21 +522,21 @@ bool Shortcut::CheckModifiersKeyboardState() const // Check the ctrl key state if (ctrlKey == ModifierKey::Left) { - if (!(GetAsyncKeyState(VK_LCONTROL) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_LCONTROL))) { return false; } } else if (ctrlKey == ModifierKey::Right) { - if (!(GetAsyncKeyState(VK_RCONTROL) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_RCONTROL))) { return false; } } else if (ctrlKey == ModifierKey::Both) { - if (!(GetAsyncKeyState(VK_CONTROL) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_CONTROL))) { return false; } @@ -545,21 +545,21 @@ bool Shortcut::CheckModifiersKeyboardState() const // Check the alt key state if (altKey == ModifierKey::Left) { - if (!(GetAsyncKeyState(VK_LMENU) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_LMENU))) { return false; } } else if (altKey == ModifierKey::Right) { - if (!(GetAsyncKeyState(VK_RMENU) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_RMENU))) { return false; } } else if (altKey == ModifierKey::Both) { - if (!(GetAsyncKeyState(VK_MENU) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_MENU))) { return false; } @@ -568,21 +568,21 @@ bool Shortcut::CheckModifiersKeyboardState() const // Check the shift key state if (shiftKey == ModifierKey::Left) { - if (!(GetAsyncKeyState(VK_LSHIFT) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_LSHIFT))) { return false; } } else if (shiftKey == ModifierKey::Right) { - if (!(GetAsyncKeyState(VK_RSHIFT) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_RSHIFT))) { return false; } } else if (shiftKey == ModifierKey::Both) { - if (!(GetAsyncKeyState(VK_SHIFT) & 0x8000)) + if (!(ii.GetVirtualKeyState(VK_SHIFT))) { return false; } @@ -592,7 +592,7 @@ bool Shortcut::CheckModifiersKeyboardState() const } // Function to check if any keys are pressed down except those in the shortcut -bool Shortcut::IsKeyboardStateClearExceptShortcut() const +bool Shortcut::IsKeyboardStateClearExceptShortcut(InputInterface& ii) const { // Iterate through all the virtual key codes - 0xFF is set to key down because of the Num Lock for (int keyVal = 1; keyVal < 0xFF; keyVal++) @@ -603,7 +603,7 @@ bool Shortcut::IsKeyboardStateClearExceptShortcut() const continue; } // Check state of the key. If the key is pressed down but it is not part of the shortcut then the keyboard state is not clear - if (GetAsyncKeyState(keyVal) & 0x8000) + if (ii.GetVirtualKeyState(keyVal)) { // If one of the win keys is pressed check if it is part of the shortcut if (keyVal == VK_LWIN) @@ -779,15 +779,15 @@ KeyboardManagerHelper::ErrorType Shortcut::DoKeysOverlap(const Shortcut& first, else if (first.actionKey == second.actionKey) { // corresponding modifiers are either both disabled or both not disabled - this ensures that both match in types of modifiers i.e. Ctrl(l/r/c) Shift (l/r/c) A matches Ctrl(l/r/c) Shift (l/r/c) A - if (((first.winKey != ModifierKey::Disabled && second.winKey != ModifierKey::Disabled) || (first.winKey == ModifierKey::Disabled && second.winKey == ModifierKey::Disabled)) && - ((first.ctrlKey != ModifierKey::Disabled && second.ctrlKey != ModifierKey::Disabled) || (first.ctrlKey == ModifierKey::Disabled && second.ctrlKey == ModifierKey::Disabled)) && - ((first.altKey != ModifierKey::Disabled && second.altKey != ModifierKey::Disabled) || (first.altKey == ModifierKey::Disabled && second.altKey == ModifierKey::Disabled)) && + if (((first.winKey != ModifierKey::Disabled && second.winKey != ModifierKey::Disabled) || (first.winKey == ModifierKey::Disabled && second.winKey == ModifierKey::Disabled)) && + ((first.ctrlKey != ModifierKey::Disabled && second.ctrlKey != ModifierKey::Disabled) || (first.ctrlKey == ModifierKey::Disabled && second.ctrlKey == ModifierKey::Disabled)) && + ((first.altKey != ModifierKey::Disabled && second.altKey != ModifierKey::Disabled) || (first.altKey == ModifierKey::Disabled && second.altKey == ModifierKey::Disabled)) && ((first.shiftKey != ModifierKey::Disabled && second.shiftKey != ModifierKey::Disabled) || (first.shiftKey == ModifierKey::Disabled && second.shiftKey == ModifierKey::Disabled))) { // If one of the modifier is common - if ((first.winKey == ModifierKey::Both || second.winKey == ModifierKey::Both) || - (first.ctrlKey == ModifierKey::Both || second.ctrlKey == ModifierKey::Both) || - (first.altKey == ModifierKey::Both || second.altKey == ModifierKey::Both) || + if ((first.winKey == ModifierKey::Both || second.winKey == ModifierKey::Both) || + (first.ctrlKey == ModifierKey::Both || second.ctrlKey == ModifierKey::Both) || + (first.altKey == ModifierKey::Both || second.altKey == ModifierKey::Both) || (first.shiftKey == ModifierKey::Both || second.shiftKey == ModifierKey::Both)) { return KeyboardManagerHelper::ErrorType::ConflictingModifierShortcut; diff --git a/src/modules/keyboardmanager/common/Shortcut.h b/src/modules/keyboardmanager/common/Shortcut.h index 66904c15fb..f7d748cfef 100644 --- a/src/modules/keyboardmanager/common/Shortcut.h +++ b/src/modules/keyboardmanager/common/Shortcut.h @@ -3,6 +3,7 @@ #include "../common/keyboard_layout.h" #include "../common/shared_constants.h" #include +#include "InputInterface.h" // Enum type to store different states of the win key enum class ModifierKey @@ -167,10 +168,10 @@ public: void SetKeyCodes(const std::vector& keys); // Function to check if all the modifiers in the shortcut have been pressed down - bool CheckModifiersKeyboardState() const; + bool CheckModifiersKeyboardState(InputInterface& ii) const; // Function to check if any keys are pressed down except those in the shortcut - bool IsKeyboardStateClearExceptShortcut() const; + bool IsKeyboardStateClearExceptShortcut(InputInterface& ii) const; // Function to get the number of modifiers that are common between the current shortcut and the shortcut in the argument int GetCommonModifiersCount(const Shortcut& input) const; diff --git a/src/modules/keyboardmanager/common/packages.config b/src/modules/keyboardmanager/common/packages.config new file mode 100644 index 0000000000..09415c07e2 --- /dev/null +++ b/src/modules/keyboardmanager/common/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/dll/Input.cpp b/src/modules/keyboardmanager/dll/Input.cpp new file mode 100644 index 0000000000..10c53cb28e --- /dev/null +++ b/src/modules/keyboardmanager/dll/Input.cpp @@ -0,0 +1,14 @@ +#include "pch.h" +#include "Input.h" + +// Function to simulate input +UINT Input::SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize) +{ + return SendInput(cInputs, pInputs, cbSize); +} + +// Function to get the state of a particular key +bool Input::GetVirtualKeyState(int key) +{ + return (GetAsyncKeyState(key) & 0x8000); +} diff --git a/src/modules/keyboardmanager/dll/Input.h b/src/modules/keyboardmanager/dll/Input.h new file mode 100644 index 0000000000..0b920b460a --- /dev/null +++ b/src/modules/keyboardmanager/dll/Input.h @@ -0,0 +1,14 @@ +#pragma once +#include + +// Class used to wrap keyboard input library methods +class Input : + public InputInterface +{ +public: + // Function to simulate input + UINT SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize); + + // Function to get the state of a particular key + bool GetVirtualKeyState(int key); +}; diff --git a/src/modules/keyboardmanager/dll/KeyboardEventHandlers.cpp b/src/modules/keyboardmanager/dll/KeyboardEventHandlers.cpp index 2be48d4a30..16aeeb68cb 100644 --- a/src/modules/keyboardmanager/dll/KeyboardEventHandlers.cpp +++ b/src/modules/keyboardmanager/dll/KeyboardEventHandlers.cpp @@ -4,7 +4,7 @@ namespace KeyboardEventHandlers { // Function to a handle a single key remap - intptr_t HandleSingleKeyRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept + __declspec(dllexport) intptr_t HandleSingleKeyRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept { // Check if the key event was generated by KeyboardManager to avoid remapping events generated by us. if (!(data->lParam->dwExtraInfo & CommonSharedConstants::KEYBOARDMANAGER_INJECTED_FLAG)) @@ -42,7 +42,7 @@ namespace KeyboardEventHandlers } lock.unlock(); - UINT res = SendInput(key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput(key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; return 1; } @@ -52,7 +52,7 @@ namespace KeyboardEventHandlers } // Function to a change a key's behavior from toggle to modifier - intptr_t HandleSingleKeyToggleToModEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept + __declspec(dllexport) intptr_t HandleSingleKeyToggleToModEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept { // Check if the key event was generated by KeyboardManager to avoid remapping events generated by us. if (!(data->lParam->dwExtraInfo & CommonSharedConstants::KEYBOARDMANAGER_INJECTED_FLAG)) @@ -82,7 +82,7 @@ namespace KeyboardEventHandlers KeyboardManagerHelper::SetKeyEvent(keyEventList, 1, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG); lock.unlock(); - UINT res = SendInput(key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput(key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; // Reset the long press flag when the key has been lifted. @@ -101,7 +101,7 @@ namespace KeyboardEventHandlers } // Function to a handle a shortcut remap - intptr_t HandleShortcutRemapEvent(LowlevelKeyboardEvent* data, std::map& reMap, std::mutex& map_mutex) noexcept + __declspec(dllexport) intptr_t HandleShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, std::map& reMap, std::mutex& map_mutex) noexcept { // The mutex should be unlocked before SendInput is called to avoid re-entry into the same mutex. More details can be found at https://github.com/microsoft/PowerToys/pull/1789#issuecomment-607555837 std::unique_lock lock(map_mutex); @@ -130,12 +130,12 @@ namespace KeyboardEventHandlers const size_t dest_size = it.second.targetShortcut.Size(); // If the shortcut has been pressed down - if (!it.second.isShortcutInvoked && it.first.CheckModifiersKeyboardState()) + if (!it.second.isShortcutInvoked && it.first.CheckModifiersKeyboardState(ii)) { if (data->lParam->vkCode == it.first.GetActionKey() && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)) { // Check if any other keys have been pressed apart from the shortcut. If true, then check for the next shortcut - if (!it.first.IsKeyboardStateClearExceptShortcut()) + if (!it.first.IsKeyboardStateClearExceptShortcut(ii)) { continue; } @@ -144,11 +144,11 @@ namespace KeyboardEventHandlers LPINPUT keyEventList; // Remember which win key was pressed initially - if (GetAsyncKeyState(VK_RWIN) & 0x8000) + if (ii.GetVirtualKeyState(VK_RWIN)) { it.second.winKeyInvoked = ModifierKey::Right; } - else if (GetAsyncKeyState(VK_LWIN) & 0x8000) + else if (ii.GetVirtualKeyState(VK_LWIN)) { it.second.winKeyInvoked = ModifierKey::Left; } @@ -248,7 +248,7 @@ namespace KeyboardEventHandlers it.second.isShortcutInvoked = true; lock.unlock(); - UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; return 1; } @@ -351,14 +351,14 @@ namespace KeyboardEventHandlers // key count can be 0 if both shortcuts have same modifiers and the action key is not held down. delete will throw an error if keyEventList is empty if (key_count > 0) { - UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; } return 1; } // The system will see the modifiers of the new shortcut as being held down because of the shortcut remap - if (it.second.targetShortcut.CheckModifiersKeyboardState()) + if (it.second.targetShortcut.CheckModifiersKeyboardState(ii)) { // Case 2: If the original shortcut is still held down the keyboard will get a key down message of the action key in the original shortcut and the new shortcut's modifiers will be held down (keys held down send repeated keydown messages) if (data->lParam->vkCode == it.first.GetActionKey() && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)) @@ -370,7 +370,7 @@ namespace KeyboardEventHandlers it.second.isShortcutInvoked = true; lock.unlock(); - UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; return 1; } @@ -385,7 +385,7 @@ namespace KeyboardEventHandlers it.second.isShortcutInvoked = true; lock.unlock(); - UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; return 1; } @@ -547,7 +547,7 @@ namespace KeyboardEventHandlers it.second.isShortcutInvoked = false; it.second.winKeyInvoked = ModifierKey::Disabled; lock.unlock(); - UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; return 1; } @@ -565,12 +565,12 @@ namespace KeyboardEventHandlers } // Function to a handle an os-level shortcut remap - intptr_t HandleOSLevelShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept + __declspec(dllexport) intptr_t HandleOSLevelShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept { // Check if the key event was generated by KeyboardManager to avoid remapping events generated by us. if (data->lParam->dwExtraInfo != KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG) { - bool result = HandleShortcutRemapEvent(data, keyboardManagerState.osLevelShortcutReMap, keyboardManagerState.osLevelShortcutReMap_mutex); + bool result = HandleShortcutRemapEvent(ii, data, keyboardManagerState.osLevelShortcutReMap, keyboardManagerState.osLevelShortcutReMap_mutex); return result; } @@ -578,7 +578,7 @@ namespace KeyboardEventHandlers } // Function to a handle an app-specific shortcut remap - intptr_t HandleAppSpecificShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept + __declspec(dllexport) intptr_t HandleAppSpecificShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept { // Check if the key event was generated by KeyboardManager to avoid remapping events generated by us. if (data->lParam->dwExtraInfo != KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG) @@ -594,7 +594,7 @@ namespace KeyboardEventHandlers if (it != keyboardManagerState.appSpecificShortcutReMap.end()) { lock.unlock(); - bool result = HandleShortcutRemapEvent(data, keyboardManagerState.appSpecificShortcutReMap[process_name], keyboardManagerState.appSpecificShortcutReMap_mutex); + bool result = HandleShortcutRemapEvent(ii, data, keyboardManagerState.appSpecificShortcutReMap[process_name], keyboardManagerState.appSpecificShortcutReMap_mutex); return result; } } @@ -603,7 +603,7 @@ namespace KeyboardEventHandlers } // Function to ensure Num Lock state does not change when it is suppressed by the low level hook - void SetNumLockToPreviousState() + void SetNumLockToPreviousState(InputInterface& ii) { // Num Lock's key state is applied before it is intercepted by low level keyboard hooks, so we have to manually set back the state when we suppress the key. This is done by sending an additional key up, key down set of messages. // We need 2 key events because after Num Lock is suppressed, key up to release num lock key and key down to revert the num lock state @@ -614,7 +614,7 @@ namespace KeyboardEventHandlers // Use the shortcut flag to ensure these are not intercepted by any remapped keys or shortcuts KeyboardManagerHelper::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, VK_NUMLOCK, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG); KeyboardManagerHelper::SetKeyEvent(keyEventList, 1, INPUT_KEYBOARD, VK_NUMLOCK, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG); - UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT)); + UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT)); delete[] keyEventList; } } diff --git a/src/modules/keyboardmanager/dll/KeyboardEventHandlers.h b/src/modules/keyboardmanager/dll/KeyboardEventHandlers.h index b802736858..bf2d8a6191 100644 --- a/src/modules/keyboardmanager/dll/KeyboardEventHandlers.h +++ b/src/modules/keyboardmanager/dll/KeyboardEventHandlers.h @@ -1,24 +1,25 @@ #pragma once #include #include +#include namespace KeyboardEventHandlers { // Function to a handle a single key remap - intptr_t HandleSingleKeyRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; + __declspec(dllexport) intptr_t HandleSingleKeyRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; // Function to a change a key's behavior from toggle to modifier - intptr_t HandleSingleKeyToggleToModEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; + __declspec(dllexport) intptr_t HandleSingleKeyToggleToModEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; // Function to a handle a shortcut remap - intptr_t HandleShortcutRemapEvent(LowlevelKeyboardEvent* data, std::map& reMap, std::mutex& map_mutex) noexcept; + __declspec(dllexport) intptr_t HandleShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, std::map& reMap, std::mutex& map_mutex) noexcept; // Function to a handle an os-level shortcut remap - intptr_t HandleOSLevelShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; + __declspec(dllexport) intptr_t HandleOSLevelShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; // Function to a handle an app-specific shortcut remap - intptr_t HandleAppSpecificShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; + __declspec(dllexport) intptr_t HandleAppSpecificShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept; // Function to ensure Num Lock state does not change when it is suppressed by the low level hook - void SetNumLockToPreviousState(); + void SetNumLockToPreviousState(InputInterface& ii); }; diff --git a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj index de4f6b2114..8fb52d2524 100644 --- a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj +++ b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj @@ -1,5 +1,6 @@ + Debug @@ -114,12 +115,14 @@ + + Create @@ -142,7 +145,18 @@ + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj.filters b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj.filters index 1d7b219fac..7fc6e2756a 100644 --- a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj.filters +++ b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj.filters @@ -10,6 +10,9 @@ Source Files + + Source Files + @@ -21,6 +24,9 @@ Header Files + + Header Files + @@ -46,4 +52,7 @@ Resource Files + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/dll/dllmain.cpp b/src/modules/keyboardmanager/dll/dllmain.cpp index 9673978637..cc8c3a3b69 100644 --- a/src/modules/keyboardmanager/dll/dllmain.cpp +++ b/src/modules/keyboardmanager/dll/dllmain.cpp @@ -14,6 +14,7 @@ #include #include #include "KeyboardEventHandlers.h" +#include "Input.h" extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -56,6 +57,9 @@ private: // Variable which stores all the state information to be shared between the UI and back-end KeyboardManagerState keyboardManagerState; + // Object of class which implements InputInterface. Required for calling library functions while enabling testing + Input inputHandler; + public: // Constructor KeyboardManager() @@ -286,7 +290,7 @@ public: // Reset Num Lock whenever a NumLock key down event is suppressed since Num Lock key state change occurs before it is intercepted by low level hooks if (event.lParam->vkCode == VK_NUMLOCK && (event.wParam == WM_KEYDOWN || event.wParam == WM_SYSKEYDOWN) && event.lParam->dwExtraInfo != KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG) { - KeyboardEventHandlers::SetNumLockToPreviousState(); + KeyboardEventHandlers::SetNumLockToPreviousState(keyboardmanager_object_ptr->inputHandler); } return 1; } @@ -347,7 +351,7 @@ public: } // Remap a key - intptr_t SingleKeyRemapResult = KeyboardEventHandlers::HandleSingleKeyRemapEvent(data, keyboardManagerState); + intptr_t SingleKeyRemapResult = KeyboardEventHandlers::HandleSingleKeyRemapEvent(inputHandler, data, keyboardManagerState); // Single key remaps have priority. If a key is remapped, only the remapped version should be visible to the shortcuts and hence the event should be suppressed here. if (SingleKeyRemapResult == 1) @@ -367,10 +371,10 @@ public: } //// Remap a key to behave like a modifier instead of a toggle - //intptr_t SingleKeyToggleToModResult = KeyboardEventHandlers::HandleSingleKeyToggleToModEvent(data, keyboardManagerState); + //intptr_t SingleKeyToggleToModResult = KeyboardEventHandlers::HandleSingleKeyToggleToModEvent(inputHandler, data, keyboardManagerState); //// Handle an app-specific shortcut remapping - //intptr_t AppSpecificShortcutRemapResult = KeyboardEventHandlers::HandleAppSpecificShortcutRemapEvent(data, keyboardManagerState); + //intptr_t AppSpecificShortcutRemapResult = KeyboardEventHandlers::HandleAppSpecificShortcutRemapEvent(inputHandler, data, keyboardManagerState); //// If an app-specific shortcut is remapped then the os-level shortcut remapping should be suppressed. //if (AppSpecificShortcutRemapResult == 1) @@ -379,7 +383,7 @@ public: //} // Handle an os-level shortcut remapping - intptr_t OSLevelShortcutRemapResult = KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent(data, keyboardManagerState); + intptr_t OSLevelShortcutRemapResult = KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent(inputHandler, data, keyboardManagerState); // If any of the supported types of remappings took place, then suppress the key event if ((SingleKeyRemapResult + OSLevelShortcutRemapResult) > 0) diff --git a/src/modules/keyboardmanager/dll/packages.config b/src/modules/keyboardmanager/dll/packages.config new file mode 100644 index 0000000000..09415c07e2 --- /dev/null +++ b/src/modules/keyboardmanager/dll/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/test/KeyboardManagerRemapLogicTest.cpp b/src/modules/keyboardmanager/test/KeyboardManagerRemapLogicTest.cpp new file mode 100644 index 0000000000..db38e2201a --- /dev/null +++ b/src/modules/keyboardmanager/test/KeyboardManagerRemapLogicTest.cpp @@ -0,0 +1,1310 @@ +#include "pch.h" +#include "CppUnitTest.h" +#include "MockedInput.h" +#include +#include +#include "TestHelpers.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +MockedInput mockedInputHandler; +KeyboardManagerState testState; + +namespace KeyboardManagerRemapLogicTests +{ + TEST_CLASS (MockedInputSanityTests) + { + public: + // Test if mocked input is working + TEST_METHOD (MockedInput_ShouldSetKeyboardState_OnKeyEvent) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Send key down and key up for A key (0x41) and check keyboard state both times + const int nInputs = 1; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = 0x41; + + // Send A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // A key state should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), true); + input[0].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send A keyup + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // A key state should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + } + }; + + TEST_CLASS (SingleKeyRemappingTests) + { + public: + // Test if correct keyboard states are set for a single key remap + TEST_METHOD (RemappedKey_ShouldSetTargetKeyState_OnKeyEvent) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleSingleKeyRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleSingleKeyRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap A to B + testState.AddSingleKeyRemap(0x41, 0x42); + const int nInputs = 1; + + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = 0x41; + + // Send A keydown + mockedInputHandler.SendVirtualInput(1, input, sizeof(INPUT)); + + // A key state should be unchanged, and B key state should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x42), true); + input[0].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send A keyup + mockedInputHandler.SendVirtualInput(1, input, sizeof(INPUT)); + + // A key state should be unchanged, and B key state should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x42), false); + } + + // Test if key is suppressed if a key is disabled by single key remap + TEST_METHOD (RemappedKeyDisabled_ShouldNotChangeKeyState_OnKeyEvent) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleSingleKeyRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleSingleKeyRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap A to 0x0 (disabled) + testState.AddSingleKeyRemap(0x41, 0x0); + const int nInputs = 1; + + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = 0x41; + + // Send A keydown + mockedInputHandler.SendVirtualInput(1, input, sizeof(INPUT)); + + // A key state should be unchanged + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + input[0].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send A keyup + mockedInputHandler.SendVirtualInput(1, input, sizeof(INPUT)); + + // A key state should be unchanged + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + } + + // Test if correct keyboard states are set for a remap to Win (Both) key + TEST_METHOD (RemappedKeyToWinBoth_ShouldSetWinLeftKeyState_OnKeyEvent) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleSingleKeyRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleSingleKeyRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap A to Common Win key + testState.AddSingleKeyRemap(0x41, CommonSharedConstants::VK_WIN_BOTH); + const int nInputs = 1; + + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = 0x41; + + // Send A keydown + mockedInputHandler.SendVirtualInput(1, input, sizeof(INPUT)); + + // A key state should be unchanged, and common Win key state should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), true); + input[0].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send A keyup + mockedInputHandler.SendVirtualInput(1, input, sizeof(INPUT)); + + // A key state should be unchanged, and common Win key state should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + } + }; + + TEST_CLASS (OSLevelShortcutRemappingTests) + { + public: + // Test if correct keyboard states are set for a 2 key shortcut remap wih different modifiers key down + TEST_METHOD (RemappedTwoKeyShortcutWithDiffModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Alt+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 2; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + + // Send Ctrl+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl and A key states should be unchanged, Alt and V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 2 key shortcut remap with same modifiers key down + TEST_METHOD (RemappedTwoKeyShortcutWithSameModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Ctrl+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 2; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + + // Send Ctrl+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // A key state should be unchanged, Ctrl and V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 2 key shortcut remap with different modifiers key down followed by key up + TEST_METHOD (RemappedTwoKeyShortcutWithDiffModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Alt+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 4; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = VK_CONTROL; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+A keydown, followed by A and Ctrl released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, A, Alt, V key states should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 2 key shortcut remap with same modifiers key down followed by key up + TEST_METHOD (RemappedTwoKeyShortcutWithSameModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Ctrl+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 4; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = VK_CONTROL; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+A keydown, followed by A and Ctrl released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, A, V key states should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set when a 2 key shortcut is remapped, and a 3 key shortcut containing those keys is invoked - Ex: Ctrl+A remapped, but user presses Ctrl+Shift+A + TEST_METHOD (RemappedTwoKeyShortcutInvokingAShortcutContainingThoseKeys_ShouldNotBeRemapped_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Alt+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 3; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + + // Send Ctrl+Shift+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Since Ctrl+Shift+A is not remapped, no remapping should be invoked + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 3 key shortcut remap wih different modifiers key down + TEST_METHOD (RemappedThreeKeyShortcutWithDiffModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Alt+LWin+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_LWIN); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 3; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + + // Send Ctrl+Shift+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, Shift, A key states should be unchanged, Alt, LWin, V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 3 key shortcut remap wih partially different modifiers key down + TEST_METHOD (RemappedThreeKeyShortcutWithPartiallyDiffModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Alt+Ctrl+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_CONTROL); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 3; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + + // Send Ctrl+Shift+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Shift, A key states should be unchanged, Alt, Ctrl, V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 3 key shortcut remap with same modifiers key down + TEST_METHOD (RemappedThreeKeyShortcutWithSameModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Ctrl+Shift+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(VK_SHIFT); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 3; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + + // Send Ctrl+Shift+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // A key state should be unchanged, Ctrl, Shift, V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 3 key shortcut remap with different modifiers key down followed by key up + TEST_METHOD (RemappedThreeKeyShortcutWithDiffModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Alt+LWin+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_LWIN); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 6; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x41; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + input[4].type = INPUT_KEYBOARD; + input[4].ki.wVk = VK_SHIFT; + input[4].ki.dwFlags = KEYEVENTF_KEYUP; + input[5].type = INPUT_KEYBOARD; + input[5].ki.wVk = VK_CONTROL; + input[5].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+Shift+A keydown, followed by A, Shift and Ctrl released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, Shift, A, Alt, LWin, V key states should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 3 key shortcut remap with partially different modifiers key down followed by key up + TEST_METHOD (RemappedThreeKeyShortcutWithPartiallyDiffModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Alt+Ctrl+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_CONTROL); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 6; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x41; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + input[4].type = INPUT_KEYBOARD; + input[4].ki.wVk = VK_SHIFT; + input[4].ki.dwFlags = KEYEVENTF_KEYUP; + input[5].type = INPUT_KEYBOARD; + input[5].ki.wVk = VK_CONTROL; + input[5].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+Shift+A keydown, followed by A, Shift and Ctrl released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, Shift, A, Alt, V key states should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 3 key shortcut remap with same modifiers key down followed by key up + TEST_METHOD (RemappedThreeKeyShortcutWithSameModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Ctrl+Shift+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(VK_SHIFT); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 6; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x41; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + input[4].type = INPUT_KEYBOARD; + input[4].ki.wVk = VK_SHIFT; + input[4].ki.dwFlags = KEYEVENTF_KEYUP; + input[5].type = INPUT_KEYBOARD; + input[5].ki.wVk = VK_CONTROL; + input[5].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+Shift+A keydown, followed by A, Shift and Ctrl released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, Shift, A, V key states should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set when a 3 key shortcut is remapped, and a 2 key shortcut which is a subset of those keys is invoked - Ex: Ctrl+Shift+A remapped, but user presses Ctrl+A + TEST_METHOD (RemappedThreeKeyShortcutInvokingAShortcutSubsetOfThoseKeys_ShouldNotBeRemapped_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Alt+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + const int nInputs = 2; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + + // Send Ctrl+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Since Ctrl+A is not remapped, no remapping should be invoked + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 3 key to 2 key shortcut remap with different modifiers key down + TEST_METHOD (RemappedThreeKeyToTwoKeyShortcutWithDiffModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Alt+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 3; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + + // Send Ctrl+Shift+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, Shift, A key states should be unchanged, Alt, V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 3 key to 2 key shortcut remap with partially different modifiers key down + TEST_METHOD (RemappedThreeKeyToTwoKeyShortcutWithPartiallyDiffModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Ctrl+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 3; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + + // Send Ctrl+Shift+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Shift, A key states should be unchanged, Ctrl, V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 3 key to 2 key shortcut remap with different modifiers key down followed by key up + TEST_METHOD (RemappedThreeKeyToTwoKeyShortcutWithDiffModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Alt+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 6; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x41; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + input[4].type = INPUT_KEYBOARD; + input[4].ki.wVk = VK_SHIFT; + input[4].ki.dwFlags = KEYEVENTF_KEYUP; + input[5].type = INPUT_KEYBOARD; + input[5].ki.wVk = VK_CONTROL; + input[5].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+Shift+A keydown, followed by A, Shift and Ctrl released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, Shift, A, Alt, V key states should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 3 key to 2 key shortcut remap with partially different modifiers key down followed by key up + TEST_METHOD (RemappedThreeKeyToTwoKeyShortcutWithPartiallyDiffModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+Shift+A to Ctrl+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(VK_SHIFT); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 6; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_SHIFT; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x41; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + input[4].type = INPUT_KEYBOARD; + input[4].ki.wVk = VK_SHIFT; + input[4].ki.dwFlags = KEYEVENTF_KEYUP; + input[5].type = INPUT_KEYBOARD; + input[5].ki.wVk = VK_CONTROL; + input[5].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+Shift+A keydown, followed by A, Shift and Ctrl released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, Shift, A, V key states should be false + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 2 key to 3 key shortcut remap with different modifiers key down + TEST_METHOD (RemappedTwoKeyToThreeKeyShortcutWithDiffModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Alt+Shift+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_SHIFT); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 2; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + + // Send Ctrl+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, A key states should be unchanged, Alt, Shift, V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 2 key to 3 key shortcut remap with partially different modifiers key down + TEST_METHOD (RemappedTwoKeyToThreeKeyShortcutWithPartiallyDiffModifiers_ShouldSetTargetShortcutDown_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Ctrl+Shift+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(VK_SHIFT); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 2; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + + // Send Ctrl+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // A key state should be unchanged, Ctrl, Shift, V key states should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + } + + // Test if correct keyboard states are set for a 2 key to 3 key shortcut remap with different modifiers key down followed by key up + TEST_METHOD (RemappedTwoKeyToThreeKeyShortcutWithDiffModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Alt+Shift+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_SHIFT); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 4; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = VK_CONTROL; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+A keydown and A, Ctrl are then released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, A, Alt, Shift, V key states should be unchanged + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set for a 2 key to 3 key shortcut remap with partially different modifiers key down followed by key up + TEST_METHOD (RemappedTwoKeyToThreeKeyShortcutWithPartiallyDiffModifiers_ShouldClearKeyboard_OnKeyUp) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Ctrl+Shift+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_CONTROL); + dest.SetKey(VK_SHIFT); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 4; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = VK_CONTROL; + input[3].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+A keydown and A, Ctrl are then released + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, A, Shift, V key states should be unchanged + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set if a shortcut remap is pressed and then an unremapped shortcut with the same modifier is pressed - Ex: Ctrl+A is remapped. User invokes Ctrl+A then releases A and presses C (while Ctrl is held), should invoke Ctrl+C + TEST_METHOD (InvokingUnremappedShortcutAfterRemappedShortcutWithSameModifier_ShouldSetUnremappedShortcut_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Alt+Shift+V + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_SHIFT); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 4; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x43; + + // Send Ctrl+A keydown, A key up, then C key down + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // A, Alt, Shift, V key states should be unchanged, Ctrl, C should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_SHIFT), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x43), true); + } + + // Test if correct keyboard states are set for a shortcut remap with win both modifier + TEST_METHOD (RemappedShortcutWithWinBothModifier_ShouldSetRemappedShortcut_OnKeyEvent) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Win+A to Alt+V + Shortcut src; + src.SetKey(CommonSharedConstants::VK_WIN_BOTH); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + // Remap Alt+D to Win+B + Shortcut dest1; + dest1.SetKey(CommonSharedConstants::VK_WIN_BOTH); + dest1.SetKey(0x42); + Shortcut src1; + src1.SetKey(VK_MENU); + src1.SetKey(0x44); + testState.AddOSLevelShortcut(src1, dest1); + + // Test 2 cases for first remap - LWin, A, A(Up), LWin(Up). RWin, A, A(Up), RWin(Up) + const int nInputs = 2; + INPUT input[nInputs] = {}; + + // Case 1.1 + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_LWIN; + input[0].ki.dwFlags = 0; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[1].ki.dwFlags = 0; + + // Send LWin+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // LWin, RWin, A key states should be unchanged, Alt, V should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = 0x41; + input[0].ki.dwFlags = KEYEVENTF_KEYUP; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_LWIN; + input[1].ki.dwFlags = KEYEVENTF_KEYUP; + + // Release LWin+A + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // LWin, RWin, A, Alt, V key states should be unchanged + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + + // Case 1.2 + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_RWIN; + input[0].ki.dwFlags = 0; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[1].ki.dwFlags = 0; + + // Send RWin+A keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // LWin, RWin, A key states should be unchanged, Alt, V should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), true); + + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = 0x41; + input[0].ki.dwFlags = KEYEVENTF_KEYUP; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_RWIN; + input[1].ki.dwFlags = KEYEVENTF_KEYUP; + + // Release RWin+A + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // LWin, RWin, A, Alt, V key states should be unchanged + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + + // Case 2 + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_MENU; + input[0].ki.dwFlags = 0; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x44; + input[1].ki.dwFlags = 0; + + // Send Alt+D keydown + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Alt, D, RWin key states should be unchanged, LWin, B should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x42), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x44), false); + + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = 0x44; + input[0].ki.dwFlags = KEYEVENTF_KEYUP; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = VK_MENU; + input[1].ki.dwFlags = KEYEVENTF_KEYUP; + + // Release Alt+D + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // LWin, RWin, B, Alt, D key states should be unchanged + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x42), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x44), false); + } + + // Test if correct keyboard states are set if a win both shortcut remap is pressed and then an unremapped shortcut with the LWin modifier is pressed + TEST_METHOD (InvokingUnremappedShortcutWithLWinAfterRemappedShortcutWithWinBothModifier_ShouldSetUnremappedShortcutWithLWinKey_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Win+A to Alt+V + Shortcut src; + src.SetKey(CommonSharedConstants::VK_WIN_BOTH); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + // LWin, A, A(Up), C(Down) + const int nInputs = 4; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_LWIN; + input[0].ki.dwFlags = 0; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[1].ki.dwFlags = 0; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x43; + input[3].ki.dwFlags = 0; + + // Send LWin+A, release A and press C + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // RWin, A, Alt, V key states should be unchanged, LWin, C should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x43), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if correct keyboard states are set if a win both shortcut remap is pressed and then an unremapped shortcut with the RWin modifier is pressed + TEST_METHOD (InvokingUnremappedShortcutWithRWinAfterRemappedShortcutWithWinBothModifier_ShouldSetUnremappedShortcutWithRWinKey_OnKeyDown) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Win+A to Alt+V + Shortcut src; + src.SetKey(CommonSharedConstants::VK_WIN_BOTH); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(0x56); + testState.AddOSLevelShortcut(src, dest); + + // RWin, A, A(Up), C(Down) + const int nInputs = 4; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_RWIN; + input[0].ki.dwFlags = 0; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[1].ki.dwFlags = 0; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + input[3].type = INPUT_KEYBOARD; + input[3].ki.wVk = 0x43; + input[3].ki.dwFlags = 0; + + // Send RWin+A, release A and press C + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // LWin, A, Alt, V key states should be unchanged, RWin, C should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_LWIN), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_RWIN), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x43), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x56), false); + } + + // Test if target modifier is still held down even if the action key of the original shortcut is released - required for Alt+Tab/Win+Space cases + TEST_METHOD (RemappedShortcutModifiers_ShouldBeDetectedAsPressed_OnReleasingActionKeyButHoldingModifiers) + { + // Reset test environment + TestHelpers::ResetTestEnv(mockedInputHandler, testState); + + // Set HandleOSLevelShortcutRemapEvent as the hook procedure + std::function currentHookProc = std::bind(&KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent, std::ref(mockedInputHandler), std::placeholders::_1, std::ref(testState)); + mockedInputHandler.SetHookProc(currentHookProc); + + // Remap Ctrl+A to Alt+Tab + Shortcut src; + src.SetKey(VK_CONTROL); + src.SetKey(0x41); + Shortcut dest; + dest.SetKey(VK_MENU); + dest.SetKey(VK_TAB); + testState.AddOSLevelShortcut(src, dest); + + const int nInputs = 3; + INPUT input[nInputs] = {}; + input[0].type = INPUT_KEYBOARD; + input[0].ki.wVk = VK_CONTROL; + input[0].ki.dwFlags = 0; + input[1].type = INPUT_KEYBOARD; + input[1].ki.wVk = 0x41; + input[1].ki.dwFlags = 0; + input[2].type = INPUT_KEYBOARD; + input[2].ki.wVk = 0x41; + input[2].ki.dwFlags = KEYEVENTF_KEYUP; + + // Send Ctrl+A, release A + mockedInputHandler.SendVirtualInput(nInputs, input, sizeof(INPUT)); + + // Ctrl, A, Tab key states should be unchanged, Alt should be true + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_CONTROL), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(0x41), false); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_MENU), true); + Assert::AreEqual(mockedInputHandler.GetVirtualKeyState(VK_TAB), false); + } + }; +} diff --git a/src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj b/src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj new file mode 100644 index 0000000000..a94c1026f5 --- /dev/null +++ b/src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj @@ -0,0 +1,140 @@ + + + + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {62173D9A-6724-4C00-A1C8-FB646480A9EC} + Win32Proj + KeyboardManagerTest + 10.0.18362.0 + NativeUnitTestProject + + + + DynamicLibrary + true + v142 + Unicode + Spectre + + + DynamicLibrary + false + v142 + true + Unicode + Spectre + + + + + + + + + + + + + + + true + $(SolutionDir)$(Platform)\$(Configuration)\modules\KeyboardManager\ + $(Platform)\$(Configuration)\obj\$(ProjectName)\ + + + false + $(SolutionDir)$(Platform)\$(Configuration)\modules\KeyboardManager\ + $(Platform)\$(Configuration)\obj\$(ProjectName)\ + + + + Level3 + true + $(VCInstallDir)UnitTest\include;$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories) + _DEBUG;%(PreprocessorDefinitions) + true + true + MultiThreadedDebug + 4002 + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Level3 + true + true + true + $(VCInstallDir)UnitTest\include;$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories) + NDEBUG;%(PreprocessorDefinitions) + true + true + MultiThreaded + 4002 + + + Windows + true + true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Use + pch.h + + + + + + + Create + + + + + + + + + + + {8affa899-0b73-49ec-8c50-0fadda57b2fc} + + + {89f34af7-1c34-4a72-aa6e-534bcf972bd9} + + + {eaf23649-ef6e-478b-980e-81fad96cca2a} + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj.filters b/src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj.filters new file mode 100644 index 0000000000..753ca8497f --- /dev/null +++ b/src/modules/keyboardmanager/test/KeyboardManagerTest.vcxproj.filters @@ -0,0 +1,45 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/test/MockedInput.cpp b/src/modules/keyboardmanager/test/MockedInput.cpp new file mode 100644 index 0000000000..15ff38d2b8 --- /dev/null +++ b/src/modules/keyboardmanager/test/MockedInput.cpp @@ -0,0 +1,130 @@ +#include "pch.h" +#include "MockedInput.h" + +// Set the keyboard hook procedure to be tested +void MockedInput::SetHookProc(std::function hookProcedure) +{ + hookProc = hookProcedure; +} + +// Function to simulate keyboard input - arguments and return value based on SendInput function (https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput) +UINT MockedInput::SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize) +{ + // Iterate over inputs + for (UINT i = 0; i < cInputs; i++) + { + LowlevelKeyboardEvent keyEvent; + + // Distinguish between key and sys key by checking if the key is either F10 (for syskeydown) or if the key message is sent while Alt is held down. SYSKEY messages are also sent if there is no window in focus, but that has not been mocked since it would require many changes. More details on key messages at https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-syskeydown + if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) + { + if (keyboardState[VK_MENU] == true) + { + keyEvent.wParam = WM_SYSKEYUP; + } + else + { + keyEvent.wParam = WM_KEYUP; + } + } + else + { + if (pInputs[i].ki.wVk == VK_F10 || keyboardState[VK_MENU] == true) + { + keyEvent.wParam = WM_SYSKEYDOWN; + } + else + { + keyEvent.wParam = WM_KEYDOWN; + } + } + KBDLLHOOKSTRUCT lParam = {}; + + // Set only vkCode and dwExtraInfo since other values are unused + lParam.vkCode = pInputs[i].ki.wVk; + lParam.dwExtraInfo = pInputs[i].ki.dwExtraInfo; + keyEvent.lParam = &lParam; + + // Call low level hook handler + intptr_t result = MockedKeyboardHook(&keyEvent); + + // Set keyboard state if the hook does not suppress the input + if (result == 0) + { + // If key up flag is set, then set keyboard state to false + keyboardState[pInputs[i].ki.wVk] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true; + + // Handling modifier key codes + switch (pInputs[i].ki.wVk) + { + case VK_CONTROL: + if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) + { + keyboardState[VK_LCONTROL] = false; + keyboardState[VK_RCONTROL] = false; + break; + } + case VK_LCONTROL: + keyboardState[VK_CONTROL] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true; + break; + case VK_RCONTROL: + keyboardState[VK_CONTROL] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true; + break; + case VK_MENU: + if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) + { + keyboardState[VK_LMENU] = false; + keyboardState[VK_RMENU] = false; + break; + } + case VK_LMENU: + keyboardState[VK_MENU] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true; + break; + case VK_RMENU: + keyboardState[VK_MENU] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true; + break; + case VK_SHIFT: + if (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) + { + keyboardState[VK_LSHIFT] = false; + keyboardState[VK_RSHIFT] = false; + break; + } + case VK_LSHIFT: + keyboardState[VK_SHIFT] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true; + break; + case VK_RSHIFT: + keyboardState[VK_SHIFT] = (pInputs[i].ki.dwFlags & KEYEVENTF_KEYUP) ? false : true; + break; + } + } + } + + return cInputs; +} + +// Function to simulate keyboard hook behavior +intptr_t MockedInput::MockedKeyboardHook(LowlevelKeyboardEvent* data) +{ + // If the hookProc is set to null, then skip the hook + if (hookProc != nullptr) + { + return hookProc(data); + } + else + { + return 0; + } +} + +// Function to get the state of a particular key +bool MockedInput::GetVirtualKeyState(int key) +{ + return keyboardState[key]; +} + +// Function to reset the mocked keyboard state +void MockedInput::ResetKeyboardState() +{ + std::fill(keyboardState.begin(), keyboardState.end(), false); +} diff --git a/src/modules/keyboardmanager/test/MockedInput.h b/src/modules/keyboardmanager/test/MockedInput.h new file mode 100644 index 0000000000..31ec836e36 --- /dev/null +++ b/src/modules/keyboardmanager/test/MockedInput.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include +#include + +// Class for mocked keyboard input +class MockedInput : + public InputInterface +{ +private: + // Stores the states for all the keys - false for key up, and true for key down + std::vector keyboardState; + + // Function to be executed as a low level hook. By default it is nullptr so the hook is skipped + std::function hookProc; + +public: + MockedInput() + { + keyboardState.resize(256, false); + } + + // Set the keyboard hook procedure to be tested + void SetHookProc(std::function hookProcedure); + + // Function to simulate keyboard input + UINT SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize); + + // Function to simulate keyboard hook behavior + intptr_t MockedKeyboardHook(LowlevelKeyboardEvent* data); + + // Function to get the state of a particular key + bool GetVirtualKeyState(int key); + + // Function to reset the mocked keyboard state + void ResetKeyboardState(); +}; diff --git a/src/modules/keyboardmanager/test/TestHelpers.cpp b/src/modules/keyboardmanager/test/TestHelpers.cpp new file mode 100644 index 0000000000..a7e5efef65 --- /dev/null +++ b/src/modules/keyboardmanager/test/TestHelpers.cpp @@ -0,0 +1,14 @@ +#include "pch.h" +#include "TestHelpers.h" + +namespace TestHelpers +{ + // Function to reset the environment variables for tests + void ResetTestEnv(MockedInput& input, KeyboardManagerState& state) + { + input.ResetKeyboardState(); + input.SetHookProc(nullptr); + state.ClearSingleKeyRemaps(); + state.ClearOSLevelShortcuts(); + } +} diff --git a/src/modules/keyboardmanager/test/TestHelpers.h b/src/modules/keyboardmanager/test/TestHelpers.h new file mode 100644 index 0000000000..30bf770c56 --- /dev/null +++ b/src/modules/keyboardmanager/test/TestHelpers.h @@ -0,0 +1,9 @@ +#pragma once +#include "MockedInput.h" +#include + +namespace TestHelpers +{ + // Function to reset the environment variables for tests + void ResetTestEnv(MockedInput& input, KeyboardManagerState& state); +} diff --git a/src/modules/keyboardmanager/test/packages.config b/src/modules/keyboardmanager/test/packages.config new file mode 100644 index 0000000000..09415c07e2 --- /dev/null +++ b/src/modules/keyboardmanager/test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/test/pch.cpp b/src/modules/keyboardmanager/test/pch.cpp new file mode 100644 index 0000000000..64b7eef6d6 --- /dev/null +++ b/src/modules/keyboardmanager/test/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/src/modules/keyboardmanager/test/pch.h b/src/modules/keyboardmanager/test/pch.h new file mode 100644 index 0000000000..6f70f09bee --- /dev/null +++ b/src/modules/keyboardmanager/test/pch.h @@ -0,0 +1 @@ +#pragma once diff --git a/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj b/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj index 4e26d428be..ada9f65490 100644 --- a/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj +++ b/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj @@ -1,6 +1,6 @@ - + Debug @@ -139,13 +139,13 @@ - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj.filters b/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj.filters index 86da96c16c..5341fdf2f1 100644 --- a/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj.filters +++ b/src/modules/keyboardmanager/ui/KeyboardManagerUI.vcxproj.filters @@ -58,9 +58,6 @@ Header Files - - - {7ccc5562-a9e1-4a3a-9f23-bdfee9ed5776} @@ -75,4 +72,7 @@ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + \ No newline at end of file diff --git a/src/modules/keyboardmanager/ui/packages.config b/src/modules/keyboardmanager/ui/packages.config index ac271e9bc7..240cb6e95c 100644 --- a/src/modules/keyboardmanager/ui/packages.config +++ b/src/modules/keyboardmanager/ui/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file