// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "WexTestClass.h" #include "CommonState.hpp" #include "globals.h" #include "../interactivity/win32/Clipboard.hpp" #include "../interactivity/inc/ServiceLocator.hpp" #include "dbcs.h" #include #ifdef BUILD_ONECORE_INTERACTIVITY #include "../../interactivity/inc/VtApiRedirection.hpp" #endif #include "../../inc/consoletaeftemplates.hpp" using namespace WEX::Common; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::Interactivity::Win32; static const WORD altScanCode = 0x38; static const WORD leftShiftScanCode = 0x2A; class ClipboardTests { TEST_CLASS(ClipboardTests); CommonState* m_state; TEST_CLASS_SETUP(ClassSetup) { m_state = new CommonState(); m_state->PrepareGlobalFont(); m_state->PrepareGlobalScreenBuffer(); m_state->PrepareGlobalInputBuffer(); return true; } TEST_CLASS_CLEANUP(ClassCleanup) { m_state->CleanupGlobalInputBuffer(); m_state->CleanupGlobalScreenBuffer(); m_state->CleanupGlobalFont(); return true; } TEST_METHOD_SETUP(MethodSetup) { m_state->FillTextBuffer(); return true; } TEST_METHOD_CLEANUP(MethodCleanup) { return true; } const UINT cRectsSelected = 4; std::vector SetupRetrieveFromBuffers(bool fLineSelection, std::vector& selection) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc. // set up and try to retrieve the first 4 rows from the buffer const auto& screenInfo = gci.GetActiveOutputBuffer(); selection.clear(); selection.emplace_back(SMALL_RECT{ 0, 0, 8, 0 }); selection.emplace_back(SMALL_RECT{ 0, 1, 14, 1 }); selection.emplace_back(SMALL_RECT{ 0, 2, 14, 2 }); selection.emplace_back(SMALL_RECT{ 0, 3, 8, 3 }); const auto& buffer = screenInfo.GetTextBuffer(); return buffer.GetText(true, fLineSelection, selection).text; } #pragma prefast(push) #pragma prefast(disable : 26006, "Specifically trying to check unterminated strings in this test.") TEST_METHOD(TestRetrieveFromBuffer) { // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc. std::vector selection; const auto text = SetupRetrieveFromBuffers(false, selection); // verify trailing bytes were trimmed // there are 2 double-byte characters in our sample string (see CommonState.hpp for sample) // the width is right - left VERIFY_ARE_EQUAL((short)wcslen(text[0].data()), selection[0].Right - selection[0].Left + 1); // since we're not in line selection, the line should be \r\n terminated PCWCHAR tempPtr = text[0].data(); tempPtr += text[0].size(); tempPtr -= 2; VERIFY_ARE_EQUAL(String(tempPtr), String(L"\r\n")); // since we're not in line selection, spaces should be trimmed from the end tempPtr = text[0].data(); tempPtr += selection[0].Right - selection[0].Left - 2; tempPtr++; VERIFY_IS_NULL(wcsrchr(tempPtr, L' ')); // final line of selection should not contain CR/LF tempPtr = text[3].data(); tempPtr += text[3].size(); tempPtr -= 2; VERIFY_ARE_NOT_EQUAL(String(tempPtr), String(L"\r\n")); } #pragma prefast(pop) TEST_METHOD(TestRetrieveLineSelectionFromBuffer) { // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc. std::vector selection; const auto text = SetupRetrieveFromBuffers(true, selection); // row 2, no wrap // no wrap row before the end should have CR/LF PCWCHAR tempPtr = text[2].data(); tempPtr += text[2].size(); tempPtr -= 2; VERIFY_ARE_EQUAL(String(tempPtr), String(L"\r\n")); // no wrap row should trim spaces at the end tempPtr = text[2].data(); VERIFY_IS_NULL(wcsrchr(tempPtr, L' ')); // row 1, wrap // wrap row before the end should *not* have CR/LF tempPtr = text[1].data(); tempPtr += text[1].size(); tempPtr -= 2; VERIFY_ARE_NOT_EQUAL(String(tempPtr), String(L"\r\n")); // wrap row should have spaces at the end tempPtr = text[1].data(); const wchar_t* ptr = wcsrchr(tempPtr, L' '); VERIFY_IS_NOT_NULL(ptr); } TEST_METHOD(CanConvertTextToInputEvents) { std::wstring wstr = L"hello world"; std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), wstr.size()); VERIFY_ARE_EQUAL(wstr.size() * 2, events.size()); IInputServices* pInputServices = ServiceLocator::LocateInputServices(); for (wchar_t wch : wstr) { std::deque keydownPattern{ true, false }; for (bool isKeyDown : keydownPattern) { VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); std::unique_ptr keyEvent; keyEvent.reset(static_cast(events.front().release())); events.pop_front(); const short keyState = pInputServices->VkKeyScanW(wch); VERIFY_ARE_NOT_EQUAL(-1, keyState); const WORD virtualScanCode = static_cast(pInputServices->MapVirtualKeyW(LOBYTE(keyState), MAPVK_VK_TO_VSC)); VERIFY_ARE_EQUAL(wch, keyEvent->GetCharData()); VERIFY_ARE_EQUAL(isKeyDown, keyEvent->IsKeyDown()); VERIFY_ARE_EQUAL(1, keyEvent->GetRepeatCount()); VERIFY_ARE_EQUAL(static_cast(0), keyEvent->GetActiveModifierKeys()); VERIFY_ARE_EQUAL(virtualScanCode, keyEvent->GetVirtualScanCode()); VERIFY_ARE_EQUAL(LOBYTE(keyState), keyEvent->GetVirtualKeyCode()); } } } TEST_METHOD(CanConvertUppercaseText) { std::wstring wstr = L"HeLlO WoRlD"; size_t uppercaseCount = 0; for (wchar_t wch : wstr) { std::isupper(wch) ? ++uppercaseCount : 0; } std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), wstr.size()); VERIFY_ARE_EQUAL((wstr.size() + uppercaseCount) * 2, events.size()); IInputServices* pInputServices = ServiceLocator::LocateInputServices(); VERIFY_IS_NOT_NULL(pInputServices); for (wchar_t wch : wstr) { std::deque keydownPattern{ true, false }; for (bool isKeyDown : keydownPattern) { Log::Comment(NoThrowString().Format(L"testing char: %C; keydown: %d", wch, isKeyDown)); VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); std::unique_ptr keyEvent; keyEvent.reset(static_cast(events.front().release())); events.pop_front(); const short keyScanError = -1; const short keyState = pInputServices->VkKeyScanW(wch); VERIFY_ARE_NOT_EQUAL(keyScanError, keyState); const WORD virtualScanCode = static_cast(pInputServices->MapVirtualKeyW(LOBYTE(keyState), MAPVK_VK_TO_VSC)); if (std::isupper(wch)) { // uppercase letters have shift key events // surrounding them, making two events per letter // (and another two for the keyup) VERIFY_IS_FALSE(events.empty()); VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType()); std::unique_ptr keyEvent2; keyEvent2.reset(static_cast(events.front().release())); events.pop_front(); const short keyState2 = pInputServices->VkKeyScanW(wch); VERIFY_ARE_NOT_EQUAL(keyScanError, keyState2); const WORD virtualScanCode2 = static_cast(pInputServices->MapVirtualKeyW(LOBYTE(keyState2), MAPVK_VK_TO_VSC)); if (isKeyDown) { // shift then letter const KeyEvent shiftDownEvent({ TRUE, 1, VK_SHIFT, leftShiftScanCode, L'\0', SHIFT_PRESSED }); VERIFY_ARE_EQUAL(shiftDownEvent, *keyEvent); const KeyEvent expectedKeyEvent({ TRUE, 1, LOBYTE(keyState2), virtualScanCode2, wch, SHIFT_PRESSED }); VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent2); } else { // letter then shift const KeyEvent expectedKeyEvent({ FALSE, 1, LOBYTE(keyState), virtualScanCode, wch, SHIFT_PRESSED }); VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent); const KeyEvent shiftUpEvent({ FALSE, 1, VK_SHIFT, leftShiftScanCode, L'\0', 0 }); VERIFY_ARE_EQUAL(shiftUpEvent, *keyEvent2); } } else { const KeyEvent expectedKeyEvent({ !!isKeyDown, 1, LOBYTE(keyState), virtualScanCode, wch, 0 }); VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent); } } } } #ifdef __INSIDE_WINDOWS TEST_METHOD(CanConvertCharsRequiringAltGr) { const std::wstring wstr = L"\x20ac"; // € char U+20AC std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), wstr.size()); // should be converted to: // 1. AltGr keydown // 2. € keydown // 3. € keyup // 4. AltGr keyup const size_t convertedSize = 4; VERIFY_ARE_EQUAL(convertedSize, events.size()); const short keyState = VkKeyScanW(wstr[0]); const WORD virtualKeyCode = LOBYTE(keyState); std::deque expectedEvents; expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); expectedEvents.push_back({ TRUE, 1, virtualKeyCode, 0, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); expectedEvents.push_back({ FALSE, 1, virtualKeyCode, 0, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) }); expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, L'\0', ENHANCED_KEY }); for (size_t i = 0; i < events.size(); ++i) { const KeyEvent currentKeyEvent = *reinterpret_cast(events[i].get()); VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i)); } } #endif TEST_METHOD(CanConvertCharsOutsideKeyboardLayout) { const std::wstring wstr = L"\xbc"; // ¼ char U+00BC const UINT outputCodepage = CP_JAPANESE; ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP = outputCodepage; std::deque> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(), wstr.size()); // should be converted to: // 1. left alt keydown // 2. 1st numpad keydown // 3. 1st numpad keyup // 4. 2nd numpad keydown // 5. 2nd numpad keyup // 6. left alt keyup const size_t convertedSize = 6; VERIFY_ARE_EQUAL(convertedSize, events.size()); std::deque expectedEvents; expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', LEFT_ALT_PRESSED }); expectedEvents.push_back({ TRUE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); expectedEvents.push_back({ FALSE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED }); expectedEvents.push_back({ TRUE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); expectedEvents.push_back({ FALSE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED }); expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, wstr[0], 0 }); for (size_t i = 0; i < events.size(); ++i) { const KeyEvent currentKeyEvent = *reinterpret_cast(events[i].get()); VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i)); } } };