| // Copyright (c) 2019 The Chromium 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 "ui/base/ime/win/tsf_text_store.h" |
| |
| #include <initguid.h> // for GUID_NULL and GUID_PROP_INPUTSCOPE |
| |
| #include <InputScope.h> |
| #include <OleCtl.h> |
| #include <wrl/client.h> |
| |
| #if defined(OS_WIN) |
| #include <vector> |
| #endif |
| |
| #include "base/memory/ref_counted.h" |
| #include "base/stl_util.h" |
| #include "base/win/scoped_com_initializer.h" |
| #include "base/win/scoped_variant.h" |
| #include "build/build_config.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/ime/dummy_input_method.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/base/ime/win/mock_tsf_bridge.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_dispatcher.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| using testing::_; |
| using testing::Invoke; |
| using testing::Return; |
| |
| namespace ui { |
| namespace { |
| |
| class MockTextInputClient : public TextInputClient { |
| public: |
| ~MockTextInputClient() {} |
| MOCK_METHOD1(SetCompositionText, void(const ui::CompositionText&)); |
| MOCK_METHOD1(ConfirmCompositionText, void(bool)); |
| MOCK_METHOD0(ClearCompositionText, void()); |
| MOCK_METHOD1(InsertText, void(const base::string16&)); |
| MOCK_METHOD1(InsertChar, void(const ui::KeyEvent&)); |
| MOCK_CONST_METHOD0(GetTextInputType, ui::TextInputType()); |
| MOCK_CONST_METHOD0(GetTextInputMode, ui::TextInputMode()); |
| MOCK_CONST_METHOD0(GetTextDirection, base::i18n::TextDirection()); |
| MOCK_CONST_METHOD0(GetTextInputFlags, int()); |
| MOCK_CONST_METHOD0(CanComposeInline, bool()); |
| MOCK_CONST_METHOD0(GetCaretBounds, gfx::Rect()); |
| MOCK_CONST_METHOD2(GetCompositionCharacterBounds, bool(uint32_t, gfx::Rect*)); |
| MOCK_CONST_METHOD0(HasCompositionText, bool()); |
| MOCK_CONST_METHOD0(GetFocusReason, ui::TextInputClient::FocusReason()); |
| MOCK_METHOD0(ShouldDoLearning, bool()); |
| MOCK_CONST_METHOD1(GetTextRange, bool(gfx::Range*)); |
| MOCK_CONST_METHOD1(GetCompositionTextRange, bool(gfx::Range*)); |
| MOCK_CONST_METHOD1(GetEditableSelectionRange, bool(gfx::Range*)); |
| MOCK_METHOD1(SetEditableSelectionRange, bool(const gfx::Range&)); |
| MOCK_METHOD1(DeleteRange, bool(const gfx::Range&)); |
| MOCK_CONST_METHOD2(GetTextFromRange, |
| bool(const gfx::Range&, base::string16*)); |
| MOCK_METHOD0(OnInputMethodChanged, void()); |
| MOCK_METHOD1(ChangeTextDirectionAndLayoutAlignment, |
| bool(base::i18n::TextDirection)); |
| MOCK_METHOD2(ExtendSelectionAndDelete, void(size_t, size_t)); |
| MOCK_METHOD1(EnsureCaretNotInRect, void(const gfx::Rect&)); |
| MOCK_CONST_METHOD1(IsTextEditCommandEnabled, bool(TextEditCommand)); |
| MOCK_METHOD1(SetTextEditCommandForNextKeyEvent, void(TextEditCommand)); |
| MOCK_CONST_METHOD0(GetClientSourceForMetrics, ukm::SourceId()); |
| MOCK_METHOD2(SetCompositionFromExistingText, |
| bool(const gfx::Range&, const std::vector<ui::ImeTextSpan>&)); |
| MOCK_METHOD3(SetActiveCompositionForAccessibility, |
| void(const gfx::Range&, const base::string16&, bool)); |
| }; |
| |
| class MockInputMethodDelegate : public internal::InputMethodDelegate { |
| public: |
| ~MockInputMethodDelegate() {} |
| MOCK_METHOD1(DispatchKeyEventPostIME, EventDispatchDetails(KeyEvent*)); |
| }; |
| |
| class MockStoreACPSink : public ITextStoreACPSink { |
| public: |
| MockStoreACPSink() : ref_count_(0) {} |
| |
| // IUnknown |
| ULONG STDMETHODCALLTYPE AddRef() override { |
| return InterlockedIncrement(&ref_count_); |
| } |
| ULONG STDMETHODCALLTYPE Release() override { |
| const LONG count = InterlockedDecrement(&ref_count_); |
| if (!count) { |
| delete this; |
| return 0; |
| } |
| return static_cast<ULONG>(count); |
| } |
| HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** report) override { |
| if (iid == IID_IUnknown || iid == IID_ITextStoreACPSink) { |
| *report = static_cast<ITextStoreACPSink*>(this); |
| } else { |
| *report = nullptr; |
| return E_NOINTERFACE; |
| } |
| AddRef(); |
| return S_OK; |
| } |
| |
| // ITextStoreACPSink |
| MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, |
| OnTextChange, |
| HRESULT(DWORD, const TS_TEXTCHANGE*)); |
| MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, OnSelectionChange, HRESULT()); |
| MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, |
| OnLayoutChange, |
| HRESULT(TsLayoutCode, TsViewCookie)); |
| MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, OnStatusChange, HRESULT(DWORD)); |
| MOCK_METHOD4_WITH_CALLTYPE(STDMETHODCALLTYPE, |
| OnAttrsChange, |
| HRESULT(LONG, LONG, ULONG, const TS_ATTRID*)); |
| MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, OnLockGranted, HRESULT(DWORD)); |
| MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, |
| OnStartEditTransaction, |
| HRESULT()); |
| MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, |
| OnEndEditTransaction, |
| HRESULT()); |
| |
| private: |
| virtual ~MockStoreACPSink() {} |
| |
| volatile LONG ref_count_; |
| }; |
| |
| class FakeInputMethod : public DummyInputMethod { |
| public: |
| FakeInputMethod() : client_(nullptr), count_show_ime_if_needed_(0) {} |
| |
| void SetFocusedTextInputClient(TextInputClient* client) override { |
| count_set_focused_text_input_client_++; |
| client_ = client; |
| } |
| |
| TextInputClient* GetTextInputClient() const override { return client_; } |
| |
| void ShowVirtualKeyboardIfEnabled() override { |
| count_show_ime_if_needed_++; |
| // Set the input policy in textstore using TSFBridge |
| tsf_bridge_->SetInputPanelPolicy(/*inputPanelPolicyManual*/ false); |
| } |
| |
| void DetachTextInputClient(TextInputClient* client) override { |
| if (client_ == client) |
| client_ = nullptr; |
| // Set the input policy in textstore using TSFBridge |
| tsf_bridge_->SetInputPanelPolicy(/*inputPanelPolicyManual*/ true); |
| } |
| |
| void OnTextInputTypeChanged(const TextInputClient* client) override { |
| count_on_text_input_type_changed_++; |
| } |
| |
| void SetTSFTextStoreForBridge(TSFTextStore* tsf_text_store) { |
| tsf_bridge_ = new MockTSFBridge(); |
| tsf_bridge_->SetTSFTextStoreForTesting(tsf_text_store); |
| } |
| |
| int count_show_ime_if_needed() const { return count_show_ime_if_needed_; } |
| |
| private: |
| TextInputClient* client_; |
| MockTSFBridge* tsf_bridge_; |
| int count_show_ime_if_needed_; |
| int count_set_focused_text_input_client_; |
| int count_on_text_input_type_changed_; |
| }; |
| |
| const HWND kWindowHandle = reinterpret_cast<HWND>(1); |
| |
| } // namespace |
| |
| class TSFInputPanelTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| text_store_ = new TSFTextStore(); |
| sink_ = new MockStoreACPSink(); |
| EXPECT_EQ(S_OK, text_store_->AdviseSink(IID_ITextStoreACPSink, sink_.get(), |
| TS_AS_ALL_SINKS)); |
| text_store_->SetFocusedTextInputClient(kWindowHandle, &text_input_client_); |
| text_store_->SetInputMethodDelegate(&input_method_delegate_); |
| fake_input_method_ = std::make_unique<FakeInputMethod>(); |
| fake_input_method_->SetTSFTextStoreForBridge(text_store_.get()); |
| } |
| |
| void TearDown() override { |
| EXPECT_EQ(S_OK, text_store_->UnadviseSink(sink_.get())); |
| sink_ = nullptr; |
| text_store_ = nullptr; |
| } |
| |
| // Accessors to the internal state of TSFTextStore. |
| |
| base::win::ScopedCOMInitializer com_initializer_; |
| MockTextInputClient text_input_client_; |
| MockInputMethodDelegate input_method_delegate_; |
| scoped_refptr<TSFTextStore> text_store_; |
| scoped_refptr<MockStoreACPSink> sink_; |
| std::unique_ptr<FakeInputMethod> fake_input_method_; |
| }; |
| |
| class TSFMultipleInputPanelTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| text_store1_ = new TSFTextStore(); |
| text_store2_ = new TSFTextStore(); |
| sink1_ = new MockStoreACPSink(); |
| sink2_ = new MockStoreACPSink(); |
| EXPECT_EQ(S_OK, text_store1_->AdviseSink(IID_ITextStoreACPSink, |
| sink1_.get(), TS_AS_ALL_SINKS)); |
| EXPECT_EQ(S_OK, text_store2_->AdviseSink(IID_ITextStoreACPSink, |
| sink2_.get(), TS_AS_ALL_SINKS)); |
| text_store1_->SetFocusedTextInputClient(kWindowHandle, |
| &text_input_client1_); |
| text_store1_->SetInputMethodDelegate(&input_method_delegate_); |
| text_store2_->SetFocusedTextInputClient(kWindowHandle, |
| &text_input_client2_); |
| text_store2_->SetInputMethodDelegate(&input_method_delegate_); |
| fake_input_method_ = std::make_unique<FakeInputMethod>(); |
| fake_input_method_->SetTSFTextStoreForBridge(text_store1_.get()); |
| } |
| |
| void SwitchToDifferentTSFTextStore() { |
| fake_input_method_->SetTSFTextStoreForBridge(text_store2_.get()); |
| } |
| |
| void TearDown() override { |
| EXPECT_EQ(S_OK, text_store1_->UnadviseSink(sink1_.get())); |
| EXPECT_EQ(S_OK, text_store2_->UnadviseSink(sink2_.get())); |
| sink1_ = nullptr; |
| sink2_ = nullptr; |
| text_store1_ = nullptr; |
| text_store2_ = nullptr; |
| } |
| |
| // Accessors to the internal state of TSFTextStore. |
| |
| base::win::ScopedCOMInitializer com_initializer_; |
| MockTextInputClient text_input_client1_; |
| MockTextInputClient text_input_client2_; |
| MockInputMethodDelegate input_method_delegate_; |
| scoped_refptr<TSFTextStore> text_store1_; |
| scoped_refptr<TSFTextStore> text_store2_; |
| scoped_refptr<MockStoreACPSink> sink1_; |
| scoped_refptr<MockStoreACPSink> sink2_; |
| std::unique_ptr<FakeInputMethod> fake_input_method_; |
| }; |
| |
| namespace { |
| |
| TEST_F(TSFInputPanelTest, GetStatusTest) { |
| TS_STATUS status = {}; |
| EXPECT_EQ(S_OK, text_store_->GetStatus(&status)); |
| EXPECT_EQ((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| } |
| |
| TEST_F(TSFInputPanelTest, ManualInputPaneToAutomaticPolicyTest) { |
| TS_STATUS status = {}; |
| EXPECT_EQ(S_OK, text_store_->GetStatus(&status)); |
| EXPECT_EQ((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| fake_input_method_->ShowVirtualKeyboardIfEnabled(); |
| EXPECT_EQ(S_OK, text_store_->GetStatus(&status)); |
| EXPECT_NE((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| } |
| |
| TEST_F(TSFInputPanelTest, AutomaticInputPaneToManualPolicyTest) { |
| TS_STATUS status = {}; |
| // Invoke the virtual keyboard through InputMethod |
| // and test if the automatic policy flag has been set or not. |
| EXPECT_EQ(S_OK, text_store_->GetStatus(&status)); |
| EXPECT_EQ((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| fake_input_method_->ShowVirtualKeyboardIfEnabled(); |
| EXPECT_EQ(S_OK, text_store_->GetStatus(&status)); |
| EXPECT_NE((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| fake_input_method_->DetachTextInputClient(nullptr); |
| EXPECT_EQ(S_OK, text_store_->GetStatus(&status)); |
| EXPECT_EQ((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| } |
| |
| TEST_F(TSFMultipleInputPanelTest, InputPaneSwitchForMultipleTSFTextStoreTest) { |
| TS_STATUS status = {}; |
| // Invoke the virtual keyboard through InputMethod |
| // and test if the automatic policy flag has been set or not. |
| EXPECT_EQ(S_OK, text_store1_->GetStatus(&status)); |
| EXPECT_EQ((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| fake_input_method_->ShowVirtualKeyboardIfEnabled(); |
| EXPECT_EQ(S_OK, text_store1_->GetStatus(&status)); |
| EXPECT_NE((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| fake_input_method_->DetachTextInputClient(nullptr); |
| SwitchToDifferentTSFTextStore(); |
| // Different TSFTextStore is in focus so manual policy should be set in the |
| // previous one |
| EXPECT_EQ(S_OK, text_store1_->GetStatus(&status)); |
| EXPECT_EQ((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| EXPECT_EQ(S_OK, text_store2_->GetStatus(&status)); |
| EXPECT_EQ((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| fake_input_method_->ShowVirtualKeyboardIfEnabled(); |
| EXPECT_EQ(S_OK, text_store2_->GetStatus(&status)); |
| EXPECT_NE((ULONG)TS_SD_INPUTPANEMANUALDISPLAYENABLE, status.dwDynamicFlags); |
| EXPECT_EQ((ULONG)(TS_SS_TRANSITORY | TS_SS_NOHIDDENTEXT), |
| status.dwStaticFlags); |
| } |
| |
| } // namespace |
| |
| } // namespace ui |