blob: a58704acee74d35c194867ade23d29d4e37cccdc [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/shell/platform/embedder/embedder.h"
#include "flutter/shell/platform/embedder/test_utils/key_codes.h"
#include "flutter/shell/platform/windows/flutter_windows_engine.h"
#include "flutter/shell/platform/windows/keyboard_key_channel_handler.h"
#include "flutter/shell/platform/windows/keyboard_key_embedder_handler.h"
#include "flutter/shell/platform/windows/keyboard_key_handler.h"
#include "flutter/shell/platform/windows/testing/engine_modifier.h"
#include "flutter/shell/platform/windows/testing/flutter_window_win32_test.h"
#include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h"
#include "flutter/shell/platform/windows/testing/test_keyboard.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <functional>
#include <vector>
using testing::_;
using testing::Invoke;
using testing::Return;
using namespace ::flutter::testing::keycodes;
namespace flutter {
namespace testing {
namespace {
constexpr SHORT kStateMaskToggled = 0x01;
constexpr SHORT kStateMaskPressed = 0x80;
static LPARAM CreateKeyEventLparam(USHORT scancode,
bool extended,
bool was_down,
USHORT repeat_count = 1,
bool context_code = 0,
bool transition_state = 0) {
return ((LPARAM(transition_state) << 31) | (LPARAM(was_down) << 30) |
(LPARAM(context_code) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) |
(LPARAM(scancode) << 16) | LPARAM(repeat_count));
}
typedef uint32_t (*MapVkToCharHandler)(uint32_t virtual_key);
uint32_t LayoutDefault(uint32_t virtual_key) {
return MapVirtualKey(virtual_key, MAPVK_VK_TO_CHAR);
}
uint32_t LayoutFrench(uint32_t virtual_key) {
switch (virtual_key) {
case 0xDD:
return 0x8000005E;
default:
return MapVirtualKey(virtual_key, MAPVK_VK_TO_CHAR);
}
}
class MockFlutterWindowWin32 : public FlutterWindowWin32,
public MockMessageQueue {
public:
typedef std::function<void(const std::u16string& text)> U16StringHandler;
MockFlutterWindowWin32(U16StringHandler on_text)
: FlutterWindowWin32(800, 600),
on_text_(std::move(on_text)),
map_vk_to_char_(LayoutDefault) {
ON_CALL(*this, GetDpiScale())
.WillByDefault(Return(this->FlutterWindowWin32::GetDpiScale()));
}
virtual ~MockFlutterWindowWin32() {}
// Prevent copying.
MockFlutterWindowWin32(MockFlutterWindowWin32 const&) = delete;
MockFlutterWindowWin32& operator=(MockFlutterWindowWin32 const&) = delete;
// Wrapper for GetCurrentDPI() which is a protected method.
UINT GetDpi() { return GetCurrentDPI(); }
LRESULT Win32DefWindowProc(HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam) override {
return kWmResultDefault;
}
// Simulates a WindowProc message from the OS.
LRESULT InjectWindowMessage(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
return Win32SendMessage(NULL, message, wparam, lparam);
}
void OnText(const std::u16string& text) override { on_text_(text); }
MOCK_METHOD1(OnDpiScale, void(unsigned int));
MOCK_METHOD2(OnResize, void(unsigned int, unsigned int));
MOCK_METHOD2(OnPointerMove, void(double, double));
MOCK_METHOD3(OnPointerDown, void(double, double, UINT));
MOCK_METHOD3(OnPointerUp, void(double, double, UINT));
MOCK_METHOD0(OnPointerLeave, void());
MOCK_METHOD0(OnSetCursor, void());
MOCK_METHOD2(OnScroll, void(double, double));
MOCK_METHOD0(GetDpiScale, float());
MOCK_METHOD0(IsVisible, bool());
MOCK_METHOD1(UpdateCursorRect, void(const Rect&));
void SetLayout(MapVkToCharHandler map_vk_to_char) {
map_vk_to_char_ =
map_vk_to_char == nullptr ? LayoutDefault : map_vk_to_char;
}
protected:
virtual BOOL Win32PeekMessage(LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg) override {
return MockMessageQueue::Win32PeekMessage(lpMsg, hWnd, wMsgFilterMin,
wMsgFilterMax, wRemoveMsg);
}
virtual uint32_t Win32MapVkToChar(uint32_t virtual_key) override {
return map_vk_to_char_(virtual_key);
}
private:
U16StringHandler on_text_;
MapVkToCharHandler map_vk_to_char_;
LRESULT Win32SendMessage(HWND hWnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) override {
return HandleMessage(message, wparam, lparam);
}
};
class TestKeystate {
public:
void Set(uint32_t virtual_key, bool pressed, bool toggled_on = false) {
state_[virtual_key] = (pressed ? kStateMaskPressed : 0) |
(toggled_on ? kStateMaskToggled : 0);
}
SHORT Get(uint32_t virtual_key) { return state_[virtual_key]; }
KeyboardKeyEmbedderHandler::GetKeyStateHandler Getter() {
return [this](uint32_t virtual_key) { return Get(virtual_key); };
}
private:
std::map<uint32_t, SHORT> state_;
};
typedef struct {
UINT cInputs;
KEYBDINPUT kbdinput;
int cbSize;
} SendInputInfo;
// A FlutterWindowsView that overrides the RegisterKeyboardHandlers function
// to register the keyboard hook handlers that can be spied upon.
class TestFlutterWindowsView : public FlutterWindowsView {
public:
TestFlutterWindowsView()
// The WindowBindingHandler is used for window size and such, and doesn't
// affect keyboard.
: FlutterWindowsView(
std::make_unique<::testing::NiceMock<MockWindowBindingHandler>>()),
redispatch_char(0) {}
uint32_t redispatch_char;
int InjectPendingEvents(MockFlutterWindowWin32* win32window,
uint32_t redispatch_char) {
std::vector<Win32Message> messages;
int num_pending_responds = pending_responds_.size();
for (const SendInputInfo& input : pending_responds_) {
const KEYBDINPUT kbdinput = input.kbdinput;
const UINT message =
(kbdinput.dwFlags & KEYEVENTF_KEYUP) ? WM_KEYUP : WM_KEYDOWN;
const bool is_key_up = kbdinput.dwFlags & KEYEVENTF_KEYUP;
const LPARAM lparam = CreateKeyEventLparam(
kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY, is_key_up);
// TODO(dkwingsmt): Don't check the message results for redispatched
// messages for now, because making them work takes non-trivial rework
// to our current structure.
// https://github.com/flutter/flutter/issues/87843 If this is resolved,
// change them to kWmResultDefault.
messages.push_back(
Win32Message{message, kbdinput.wVk, lparam, kWmResultDontCheck});
if (redispatch_char != 0 && (kbdinput.dwFlags & KEYEVENTF_KEYUP) == 0) {
messages.push_back(
Win32Message{WM_CHAR, redispatch_char, lparam, kWmResultDontCheck});
}
}
win32window->InjectMessageList(messages.size(), messages.data());
pending_responds_.clear();
return num_pending_responds;
}
void SetKeyState(uint32_t key, bool pressed, bool toggled_on) {
key_state_.Set(key, pressed, toggled_on);
}
protected:
void RegisterKeyboardHandlers(
BinaryMessenger* messenger,
KeyboardKeyHandler::EventDispatcher dispatch_event,
KeyboardKeyEmbedderHandler::GetKeyStateHandler get_key_state) override {
FlutterWindowsView::RegisterKeyboardHandlers(
messenger,
[this](UINT cInputs, LPINPUT pInputs, int cbSize) -> UINT {
return this->SendInput(cInputs, pInputs, cbSize);
},
key_state_.Getter());
}
private:
UINT SendInput(UINT cInputs, LPINPUT pInputs, int cbSize) {
pending_responds_.push_back({cInputs, pInputs->ki, cbSize});
return 1;
}
std::vector<SendInputInfo> pending_responds_;
TestKeystate key_state_;
};
typedef enum {
kKeyCallOnKey,
kKeyCallOnText,
} KeyCallType;
typedef struct {
KeyCallType type;
// Only one of the following fields should be assigned.
FlutterKeyEvent key_event;
std::u16string text;
} KeyCall;
static std::vector<KeyCall> key_calls;
void clear_key_calls() {
for (KeyCall& key_call : key_calls) {
if (key_call.type == kKeyCallOnKey &&
key_call.key_event.character != nullptr) {
delete[] key_call.key_event.character;
}
}
key_calls.clear();
}
std::unique_ptr<FlutterWindowsEngine> GetTestEngine();
class KeyboardTester {
public:
explicit KeyboardTester() {
view_ = std::make_unique<TestFlutterWindowsView>();
view_->SetEngine(std::move(GetTestEngine()));
window_ = std::make_unique<MockFlutterWindowWin32>(
[](const std::u16string& text) {
key_calls.push_back(KeyCall{
.type = kKeyCallOnText,
.text = text,
});
});
window_->SetView(view_.get());
}
void SetKeyState(uint32_t key, bool pressed, bool toggled_on) {
view_->SetKeyState(key, pressed, toggled_on);
}
void Responding(bool response) { test_response = response; }
void SetLayout(MapVkToCharHandler layout) { window_->SetLayout(layout); }
void InjectMessages(int count, Win32Message message1, ...) {
Win32Message messages[count];
messages[0] = message1;
va_list args;
va_start(args, message1);
for (int i = 1; i < count; i += 1) {
messages[i] = va_arg(args, Win32Message);
}
va_end(args);
window_->InjectMessageList(count, messages);
}
// Inject all events called with |SendInput| to the event queue,
// then process the event queue.
//
// Returns the number of events injected.
//
// If |redispatch_char| is not 0, then WM_KEYDOWN events will
// also redispatch a WM_CHAR event with that value as lparam.
int InjectPendingEvents(uint32_t redispatch_char = 0) {
return view_->InjectPendingEvents(window_.get(), redispatch_char);
}
static bool test_response;
private:
std::unique_ptr<TestFlutterWindowsView> view_;
std::unique_ptr<MockFlutterWindowWin32> window_;
};
bool KeyboardTester::test_response = false;
// Returns an engine instance configured with dummy project path values, and
// overridden methods for sending platform messages, so that the engine can
// respond as if the framework were connected.
std::unique_ptr<FlutterWindowsEngine> GetTestEngine() {
FlutterDesktopEngineProperties properties = {};
properties.assets_path = L"C:\\foo\\flutter_assets";
properties.icu_data_path = L"C:\\foo\\icudtl.dat";
properties.aot_library_path = L"C:\\foo\\aot.so";
FlutterProjectBundle project(properties);
auto engine = std::make_unique<FlutterWindowsEngine>(project);
EngineModifier modifier(engine.get());
MockEmbedderApiForKeyboard(
modifier, [] { return KeyboardTester::test_response; },
[](const FlutterKeyEvent* event) {
FlutterKeyEvent clone_event = *event;
clone_event.character = event->character == nullptr
? nullptr
: clone_string(event->character);
key_calls.push_back(KeyCall{
.type = kKeyCallOnKey,
.key_event = clone_event,
});
return KeyboardTester::test_response;
});
engine->RunWithEntrypoint(nullptr);
return engine;
}
constexpr uint64_t kScanCodeKeyA = 0x1e;
constexpr uint64_t kScanCodeKeyE = 0x12;
constexpr uint64_t kScanCodeKeyQ = 0x10;
constexpr uint64_t kScanCodeKeyW = 0x11;
constexpr uint64_t kScanCodeDigit1 = 0x02;
// constexpr uint64_t kScanCodeNumpad1 = 0x4f;
// constexpr uint64_t kScanCodeNumLock = 0x45;
constexpr uint64_t kScanCodeControl = 0x1d;
constexpr uint64_t kScanCodeAlt = 0x38;
constexpr uint64_t kScanCodeShiftLeft = 0x2a;
// constexpr uint64_t kScanCodeShiftRight = 0x36;
constexpr uint64_t kScanCodeBracketLeft = 0x1a;
constexpr uint64_t kVirtualDigit1 = 0x31;
constexpr uint64_t kVirtualKeyA = 0x41;
constexpr uint64_t kVirtualKeyE = 0x45;
constexpr uint64_t kVirtualKeyQ = 0x51;
constexpr uint64_t kVirtualKeyW = 0x57;
constexpr bool kSynthesized = true;
constexpr bool kNotSynthesized = false;
} // namespace
// Define compound `expect` in macros. If they're defined in functions, the
// stacktrace wouldn't print where the function is called in the unit tests.
#define EXPECT_CALL_IS_EVENT(_key_call, ...) \
EXPECT_EQ(_key_call.type, kKeyCallOnKey); \
EXPECT_EVENT_EQUALS(_key_call.key_event, __VA_ARGS__);
#define EXPECT_CALL_IS_TEXT(_key_call, u16_string) \
EXPECT_EQ(_key_call.type, kKeyCallOnText); \
EXPECT_EQ(_key_call.text, u16_string);
TEST(KeyboardTest, LowerCaseAHandled) {
KeyboardTester tester;
tester.Responding(true);
// US Keyboard layout
// Press A
tester.InjectMessages(
2,
WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{'a', kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA,
kLogicalKeyA, "a", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents('a');
EXPECT_EQ(key_calls.size(), 0);
// Release A
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA,
kLogicalKeyA, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
}
TEST(KeyboardTest, LowerCaseAUnhandled) {
KeyboardTester tester;
tester.Responding(false);
// US Keyboard layout
// Press A
tester.InjectMessages(
2,
WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{'a', kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA,
kLogicalKeyA, "a", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents('a');
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_TEXT(key_calls[0], u"a");
clear_key_calls();
// Release A
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA,
kLogicalKeyA, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
}
// Press Shift-A. This is special because Win32 gives 'A' as character for the
// KeyA press.
TEST(KeyboardTest, ShiftLeftKeyA) {
KeyboardTester tester;
tester.Responding(false);
// US Keyboard layout
// Press ShiftLeft
tester.SetKeyState(VK_LSHIFT, true, true);
tester.InjectMessages(
1,
WmKeyDownInfo{VK_SHIFT, kScanCodeShiftLeft, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown,
kPhysicalShiftLeft, kLogicalShiftLeft, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Press A
tester.InjectMessages(
2,
WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{'A', kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA,
kLogicalKeyA, "A", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents('A');
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_TEXT(key_calls[0], u"A");
clear_key_calls();
// Release ShiftLeft
tester.SetKeyState(VK_LSHIFT, false, true);
tester.InjectMessages(
1, WmKeyUpInfo{VK_SHIFT, kScanCodeShiftLeft, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalShiftLeft,
kLogicalShiftLeft, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Release A
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA,
kLogicalKeyA, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
}
// Press Ctrl-A. This is special because Win32 gives 0x01 as character for the
// KeyA press.
TEST(KeyboardTest, CtrlLeftKeyA) {
KeyboardTester tester;
tester.Responding(false);
// US Keyboard layout
// Press ControlLeft
tester.SetKeyState(VK_LCONTROL, true, true);
tester.InjectMessages(
1,
WmKeyDownInfo{VK_CONTROL, kScanCodeControl, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown,
kPhysicalControlLeft, kLogicalControlLeft, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Press A
tester.InjectMessages(
2,
WmKeyDownInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{0x01, kScanCodeKeyA, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyA,
kLogicalKeyA, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents(0);
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Release A
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualKeyA, kScanCodeKeyA, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyA,
kLogicalKeyA, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
// Release ControlLeft
tester.SetKeyState(VK_LCONTROL, false, true);
tester.InjectMessages(
1, WmKeyUpInfo{VK_CONTROL, kScanCodeControl, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp,
kPhysicalControlLeft, kLogicalControlLeft, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
}
// Press Ctrl-1. This is special because it yields no WM_CHAR for the 1.
TEST(KeyboardTest, CtrlLeftDigit1) {
KeyboardTester tester;
tester.Responding(false);
// US Keyboard layout
// Press ControlLeft
tester.SetKeyState(VK_LCONTROL, true, true);
tester.InjectMessages(
1,
WmKeyDownInfo{VK_CONTROL, kScanCodeControl, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown,
kPhysicalControlLeft, kLogicalControlLeft, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Press 1
tester.InjectMessages(
1, WmKeyDownInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended, kWasUp}
.Build(kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalDigit1,
kLogicalDigit1, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents(0);
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Release 1
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalDigit1,
kLogicalDigit1, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
// Release ControlLeft
tester.SetKeyState(VK_LCONTROL, false, true);
tester.InjectMessages(
1, WmKeyUpInfo{VK_CONTROL, kScanCodeControl, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp,
kPhysicalControlLeft, kLogicalControlLeft, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
}
// Press 1 on a French keyboard. This is special because it yields WM_CHAR
// with char_code '&'.
TEST(KeyboardTest, Digit1OnFrenchLayout) {
KeyboardTester tester;
tester.Responding(false);
tester.SetLayout(LayoutFrench);
// Press 1
tester.InjectMessages(
2,
WmKeyDownInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended, kWasUp}
.Build(kWmResultZero),
WmCharInfo{'&', kScanCodeDigit1, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalDigit1,
kLogicalDigit1, "&", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents('&');
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_TEXT(key_calls[0], u"&");
clear_key_calls();
// Release 1
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalDigit1,
kLogicalDigit1, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
}
// This tests AltGr-Q on a German keyboard, which should print '@'.
TEST(KeyboardTest, AltGrModifiedKey) {
KeyboardTester tester;
tester.Responding(false);
// German Keyboard layout
// Press AltGr, which Win32 precedes with a ContrlLeft down.
tester.SetKeyState(VK_LCONTROL, true, true);
tester.InjectMessages(
2,
WmKeyDownInfo{VK_LCONTROL, kScanCodeControl, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmKeyDownInfo{VK_MENU, kScanCodeAlt, kExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 2);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown,
kPhysicalControlLeft, kLogicalControlLeft, "",
kNotSynthesized);
EXPECT_CALL_IS_EVENT(key_calls[1], kFlutterKeyEventTypeDown,
kPhysicalAltRight, kLogicalAltRight, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Press Q
tester.InjectMessages(
2,
WmKeyDownInfo{kVirtualKeyQ, kScanCodeKeyQ, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{'@', kScanCodeKeyQ, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyQ,
kLogicalKeyQ, "@", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents('@');
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_TEXT(key_calls[0], u"@");
clear_key_calls();
// Release Q
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualKeyQ, kScanCodeKeyQ, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyQ,
kLogicalKeyQ, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
// Release AltGr. Win32 doesn't dispatch ControlLeft up. Instead Flutter will
// dispatch one. The AltGr is a system key, so will be handled by Win32's
// default WndProc.
tester.InjectMessages(
1,
WmSysKeyUpInfo{VK_MENU, kScanCodeAlt, kExtended}.Build(kWmResultDefault));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalAltRight,
kLogicalAltRight, "", kNotSynthesized);
clear_key_calls();
tester.SetKeyState(VK_LCONTROL, false, false);
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp,
kPhysicalControlLeft, kLogicalControlLeft, "",
kNotSynthesized);
clear_key_calls();
}
// This tests dead key ^ then E on a French keyboard, which should be combined
// into ê.
TEST(KeyboardTest, DeadKeyThatCombines) {
KeyboardTester tester;
tester.Responding(false);
tester.SetLayout(LayoutFrench);
// Press ^¨ (US: Left bracket)
tester.InjectMessages(
2,
WmKeyDownInfo{0xDD, kScanCodeBracketLeft, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmDeadCharInfo{'^', kScanCodeBracketLeft, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown,
kPhysicalBracketLeft, kLogicalBracketRight, "^",
kNotSynthesized);
clear_key_calls();
EXPECT_EQ(tester.InjectPendingEvents(), 0);
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Release ^¨
tester.InjectMessages(
1, WmKeyUpInfo{0xDD, kScanCodeBracketLeft, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp,
kPhysicalBracketLeft, kLogicalBracketRight, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Press E
tester.InjectMessages(
2,
WmKeyDownInfo{kVirtualKeyE, kScanCodeKeyE, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{0xEA, kScanCodeKeyE, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyE,
kLogicalKeyE, "ê", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents(
0xEA); // The redispatched event uses unmodified 'e'
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_TEXT(key_calls[0], u"ê");
clear_key_calls();
// Release E
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualKeyE, kScanCodeKeyE, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyE,
kLogicalKeyE, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
}
// This tests dead key ^ then & (US: 1) on a French keyboard, which do not
// combine and should output "^$".
TEST(KeyboardTest, DeadKeyThatDoesNotCombine) {
KeyboardTester tester;
tester.Responding(false);
tester.SetLayout(LayoutFrench);
// Press ^¨ (US: Left bracket)
tester.InjectMessages(
2,
WmKeyDownInfo{0xDD, kScanCodeBracketLeft, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmDeadCharInfo{'^', kScanCodeBracketLeft, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown,
kPhysicalBracketLeft, kLogicalBracketRight, "^",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents(0); // No WM_DEADCHAR messages sent here.
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Release ^¨
tester.InjectMessages(
1, WmKeyUpInfo{0xDD, kScanCodeBracketLeft, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp,
kPhysicalBracketLeft, kLogicalBracketRight, "",
kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Press 1
tester.InjectMessages(
3,
WmKeyDownInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended, kWasUp}
.Build(kWmResultZero),
WmCharInfo{'^', kScanCodeDigit1, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{'&', kScanCodeDigit1, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 2);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalDigit1,
kLogicalDigit1, "^", kNotSynthesized);
EXPECT_CALL_IS_TEXT(key_calls[1], u"^");
clear_key_calls();
tester.InjectPendingEvents('&');
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_TEXT(key_calls[0], u"&");
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
// Release 1
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualDigit1, kScanCodeDigit1, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalDigit1,
kLogicalDigit1, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
}
// This tests when the resulting character needs to be combined with surrogates.
TEST(KeyboardTest, MultibyteCharacter) {
KeyboardTester tester;
tester.Responding(false);
// Gothic Keyboard layout. (We need a layout that yields non-BMP characters
// without IME, which that is actually very rare.)
// Press key W of a US keyboard, which should yield character '𐍅'.
tester.InjectMessages(
3,
WmKeyDownInfo{kVirtualKeyW, kScanCodeKeyW, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{0xd800, kScanCodeKeyW, kNotExtended, kWasUp}.Build(
kWmResultZero),
WmCharInfo{0xdf45, kScanCodeKeyW, kNotExtended, kWasUp}.Build(
kWmResultZero));
const char* st = key_calls[0].key_event.character;
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeDown, kPhysicalKeyW,
kLogicalKeyW, "𐍅", kNotSynthesized);
clear_key_calls();
// Inject the redispatched high surrogate.
tester.InjectPendingEvents(0xd800);
// Manually inject the redispatched low surrogate.
tester.InjectMessages(
1, WmCharInfo{0xdf45, kScanCodeKeyW, kNotExtended, kWasUp}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_TEXT(key_calls[0], u"𐍅");
clear_key_calls();
// Release W
tester.InjectMessages(
1, WmKeyUpInfo{kVirtualKeyW, kScanCodeKeyW, kNotExtended}.Build(
kWmResultZero));
EXPECT_EQ(key_calls.size(), 1);
EXPECT_CALL_IS_EVENT(key_calls[0], kFlutterKeyEventTypeUp, kPhysicalKeyW,
kLogicalKeyW, "", kNotSynthesized);
clear_key_calls();
tester.InjectPendingEvents();
EXPECT_EQ(key_calls.size(), 0);
clear_key_calls();
}
} // namespace testing
} // namespace flutter