// 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/common/cpp/json_message_codec.h" #include "flutter/shell/platform/embedder/embedder.h" #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" #include "flutter/shell/platform/windows/flutter_windows_engine.h" #include "flutter/shell/platform/windows/testing/engine_embedder_api_modifier.h" #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" #include "flutter/shell/platform/windows/testing/win32_flutter_window_test.h" #include "flutter/shell/platform/windows/text_input_plugin_delegate.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include using testing::_; using testing::Invoke; namespace flutter { namespace testing { namespace { // Creates a valid Windows LPARAM for WM_KEYDOWN and WM_CHAR from parameters // given. static LPARAM CreateKeyEventLparam(USHORT ScanCode, bool extended = false, USHORT RepeatCount = 1, bool ContextCode = 0, bool PreviousKeyState = 1, bool TransitionState = 1) { return ((LPARAM(TransitionState) << 31) | (LPARAM(PreviousKeyState) << 30) | (LPARAM(ContextCode) << 29) | (LPARAM(extended ? 0x1 : 0x0) << 24) | (LPARAM(ScanCode) << 16) | LPARAM(RepeatCount)); } // A struc to hold simulated events that will be delivered after the framework // response is handled. struct SimulatedEvent { UINT message; WPARAM wparam; LPARAM lparam; }; // A key event handler that can be spied on while it forwards calls to the real // key event handler. class SpyKeyEventHandler : public KeyboardHookHandler { public: SpyKeyEventHandler(flutter::BinaryMessenger* messenger, KeyEventHandler::SendInputDelegate delegate) { real_implementation_ = std::make_unique(messenger, delegate); ON_CALL(*this, KeyboardHook(_, _, _, _, _, _)) .WillByDefault( Invoke(real_implementation_.get(), &KeyEventHandler::KeyboardHook)); ON_CALL(*this, TextHook(_, _)) .WillByDefault( Invoke(real_implementation_.get(), &KeyEventHandler::TextHook)); } MOCK_METHOD6(KeyboardHook, bool(FlutterWindowsView* window, int key, int scancode, int action, char32_t character, bool extended)); MOCK_METHOD2(TextHook, void(FlutterWindowsView* window, const std::u16string& text)); MOCK_METHOD0(ComposeBeginHook, void()); MOCK_METHOD0(ComposeEndHook, void()); MOCK_METHOD2(ComposeChangeHook, void(const std::u16string& text, int cursor_pos)); private: std::unique_ptr real_implementation_; }; // A text input plugin that can be spied on while it forwards calls to the real // text input plugin. class SpyTextInputPlugin : public KeyboardHookHandler, public TextInputPluginDelegate { public: SpyTextInputPlugin(flutter::BinaryMessenger* messenger) { real_implementation_ = std::make_unique(messenger, this); ON_CALL(*this, KeyboardHook(_, _, _, _, _, _)) .WillByDefault( Invoke(real_implementation_.get(), &TextInputPlugin::KeyboardHook)); ON_CALL(*this, TextHook(_, _)) .WillByDefault( Invoke(real_implementation_.get(), &TextInputPlugin::TextHook)); } MOCK_METHOD6(KeyboardHook, bool(FlutterWindowsView* window, int key, int scancode, int action, char32_t character, bool extended)); MOCK_METHOD2(TextHook, void(FlutterWindowsView* window, const std::u16string& text)); MOCK_METHOD0(ComposeBeginHook, void()); MOCK_METHOD0(ComposeEndHook, void()); MOCK_METHOD2(ComposeChangeHook, void(const std::u16string& text, int cursor_pos)); virtual void OnCursorRectUpdated(const Rect& rect) {} private: std::unique_ptr real_implementation_; }; class MockWin32FlutterWindow : public Win32FlutterWindow { public: MockWin32FlutterWindow() : Win32FlutterWindow(800, 600) {} virtual ~MockWin32FlutterWindow() {} // Prevent copying. MockWin32FlutterWindow(MockWin32FlutterWindow const&) = delete; MockWin32FlutterWindow& operator=(MockWin32FlutterWindow const&) = delete; // Wrapper for GetCurrentDPI() which is a protected method. UINT GetDpi() { return GetCurrentDPI(); } // Simulates a WindowProc message from the OS. LRESULT InjectWindowMessage(UINT const message, WPARAM const wparam, LPARAM const lparam) { return HandleMessage(message, wparam, lparam); } 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)); }; // A FlutterWindowsView that overrides the RegisterKeyboardHookHandlers function // to register the keyboard hook handlers that can be spied upon. class TestFlutterWindowsView : public FlutterWindowsView { public: TestFlutterWindowsView(std::unique_ptr window_binding, WPARAM virtual_key, bool is_printable = true) : FlutterWindowsView(std::move(window_binding)), virtual_key_(virtual_key), is_printable_(is_printable) {} SpyKeyEventHandler* key_event_handler; SpyTextInputPlugin* text_input_plugin; void InjectPendingEvents(MockWin32FlutterWindow* win32window) { while (pending_events_.size() > 0) { SimulatedEvent event = pending_events_.front(); win32window->InjectWindowMessage(event.message, event.wparam, event.lparam); pending_events_.pop_front(); } } protected: void RegisterKeyboardHookHandlers( flutter::BinaryMessenger* messenger) override { auto spy_key_event_handler = std::make_unique( messenger, [this](UINT cInputs, LPINPUT pInputs, int cbSize) -> UINT { return this->SendInput(cInputs, pInputs, cbSize); }); auto spy_text_input_plugin = std::make_unique(messenger); key_event_handler = spy_key_event_handler.get(); text_input_plugin = spy_text_input_plugin.get(); AddKeyboardHookHandler(std::move(spy_key_event_handler)); AddKeyboardHookHandler(std::move(spy_text_input_plugin)); } private: UINT SendInput(UINT cInputs, LPINPUT pInputs, int cbSize) { // Simulate the event loop by just sending the event sent to // "SendInput" directly to the window. const KEYBDINPUT kbdinput = pInputs->ki; const UINT message = (kbdinput.dwFlags & KEYEVENTF_KEYUP) ? WM_KEYUP : WM_KEYDOWN; const LPARAM lparam = CreateKeyEventLparam( kbdinput.wScan, kbdinput.dwFlags & KEYEVENTF_EXTENDEDKEY); // Windows would normally fill in the virtual key code for us, so we // simulate it for the test with the key we know is in the test. The // KBDINPUT we're passed doesn't have it filled in (on purpose, so that // Windows will fill it in). pending_events_.push_back(SimulatedEvent{message, virtual_key_, lparam}); if (is_printable_ && (kbdinput.dwFlags & KEYEVENTF_KEYUP) == 0) { pending_events_.push_back(SimulatedEvent{WM_CHAR, virtual_key_, lparam}); } return 1; } std::deque pending_events_; WPARAM virtual_key_; bool is_printable_; }; // A struct to use as a FlutterPlatformMessageResponseHandle so it can keep the // callbacks and user data passed to the engine's // PlatformMessageCreateResponseHandle for use in the SendPlatformMessage // overridden function. struct TestResponseHandle { FlutterDesktopBinaryReply callback; void* user_data; }; // The static value to return as the "handled" value from the framework for key // events. Individual tests set this to change the framework response that the // test engine simulates. static bool 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 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(project); EngineEmbedderApiModifier modifier(engine.get()); // Force the non-AOT path unless overridden by the test. modifier.embedder_api().RunsAOTCompiledDartCode = []() { return false; }; modifier.embedder_api().PlatformMessageCreateResponseHandle = [](auto engine, auto data_callback, auto user_data, auto response_out) { TestResponseHandle* response_handle = new TestResponseHandle(); response_handle->user_data = user_data; response_handle->callback = data_callback; *response_out = reinterpret_cast( response_handle); return kSuccess; }; modifier.embedder_api().SendPlatformMessage = [](FLUTTER_API_SYMBOL(FlutterEngine) engine, const FlutterPlatformMessage* message) { rapidjson::Document document; auto& allocator = document.GetAllocator(); document.SetObject(); document.AddMember("handled", test_response, allocator); auto encoded = flutter::JsonMessageCodec::GetInstance().EncodeMessage(document); const TestResponseHandle* response_handle = reinterpret_cast( message->response_handle); if (response_handle->callback != nullptr) { response_handle->callback(encoded->data(), encoded->size(), response_handle->user_data); } return kSuccess; }; modifier.embedder_api().PlatformMessageReleaseResponseHandle = [](FLUTTER_API_SYMBOL(FlutterEngine) engine, FlutterPlatformMessageResponseHandle* response) { const TestResponseHandle* response_handle = reinterpret_cast(response); delete response_handle; return kSuccess; }; return engine; } } // namespace TEST(Win32FlutterWindowTest, CreateDestroy) { Win32FlutterWindowTest window(800, 600); ASSERT_TRUE(TRUE); } // Tests key event propagation of non-printable, non-modifier key down events. TEST(Win32FlutterWindowTest, NonPrintableKeyDownPropagation) { constexpr WPARAM virtual_key = VK_LEFT; constexpr WPARAM scan_code = 10; constexpr char32_t character = 0; MockWin32FlutterWindow win32window; std::deque pending_events; auto window_binding_handler = std::make_unique<::testing::NiceMock>(); TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, false /* is_printable */); win32window.SetView(&flutter_windows_view); LPARAM lparam = CreateKeyEventLparam(scan_code); // Test an event not handled by the framework { test_response = false; flutter_windows_view.SetEngine(std::move(GetTestEngine())); EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, false /* extended */)) .Times(2) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) .Times(0); EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(0); EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), 0); flutter_windows_view.InjectPendingEvents(&win32window); } // Test an event handled by the framework { test_response = true; EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, false /* extended */)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _)) .Times(0); EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), 0); flutter_windows_view.InjectPendingEvents(&win32window); } } // Tests key event propagation of printable character key down events. These // differ from non-printable characters in that they follow a different code // path in the WndProc (HandleMessage), producing a follow-on WM_CHAR event. TEST(Win32FlutterWindowTest, CharKeyDownPropagation) { constexpr WPARAM virtual_key = 65; // The "A" key, which produces a character constexpr WPARAM scan_code = 30; constexpr char32_t character = 65; MockWin32FlutterWindow win32window; auto window_binding_handler = std::make_unique<::testing::NiceMock>(); TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, true /* is_printable */); win32window.SetView(&flutter_windows_view); LPARAM lparam = CreateKeyEventLparam(scan_code); // Test an event not handled by the framework { test_response = false; flutter_windows_view.SetEngine(std::move(GetTestEngine())); EXPECT_CALL( *flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, false)) .Times(2) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(1) .RetiresOnSaturation(); EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), 0); EXPECT_EQ(win32window.InjectWindowMessage(WM_CHAR, virtual_key, lparam), 0); flutter_windows_view.InjectPendingEvents(&win32window); } // Test an event handled by the framework { test_response = true; EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, false /* is_printable */)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _)) .Times(0); EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) .Times(0); EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(0); EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), 0); EXPECT_EQ(win32window.InjectWindowMessage(WM_CHAR, virtual_key, lparam), 0); flutter_windows_view.InjectPendingEvents(&win32window); } } // Tests key event propagation of modifier key down events. This are different // from non-printable events in that they call MapVirtualKey, resulting in a // slightly different code path. TEST(Win32FlutterWindowTest, ModifierKeyDownPropagation) { constexpr WPARAM virtual_key = VK_LSHIFT; constexpr WPARAM scan_code = 20; constexpr char32_t character = 0; MockWin32FlutterWindow win32window; std::deque pending_events; auto window_binding_handler = std::make_unique<::testing::NiceMock>(); TestFlutterWindowsView flutter_windows_view( std::move(window_binding_handler), virtual_key, false /* is_printable */); win32window.SetView(&flutter_windows_view); LPARAM lparam = CreateKeyEventLparam(scan_code); // Test an event not handled by the framework { test_response = false; flutter_windows_view.SetEngine(std::move(GetTestEngine())); EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, false /* extended */)) .Times(2) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.key_event_handler, TextHook(_, _)) .Times(0); EXPECT_CALL(*flutter_windows_view.text_input_plugin, TextHook(_, _)) .Times(0); EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), 0); flutter_windows_view.InjectPendingEvents(&win32window); } // Test an event handled by the framework { test_response = true; EXPECT_CALL(*flutter_windows_view.key_event_handler, KeyboardHook(_, virtual_key, scan_code, WM_KEYDOWN, character, false /* extended */)) .Times(1) .RetiresOnSaturation(); EXPECT_CALL(*flutter_windows_view.text_input_plugin, KeyboardHook(_, _, _, _, _, _)) .Times(0); EXPECT_EQ(win32window.InjectWindowMessage(WM_KEYDOWN, virtual_key, lparam), 0); flutter_windows_view.InjectPendingEvents(&win32window); } } } // namespace testing } // namespace flutter