| // Copyright 2018 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 "components/exo/text_input.h" | 
 |  | 
 | #include <memory> | 
 | #include <string> | 
 |  | 
 | #include "base/strings/utf_string_conversions.h" | 
 | #include "components/exo/buffer.h" | 
 | #include "components/exo/shell_surface.h" | 
 | #include "components/exo/surface.h" | 
 | #include "components/exo/test/exo_test_base.h" | 
 | #include "components/exo/test/exo_test_helper.h" | 
 | #include "testing/gmock/include/gmock/gmock.h" | 
 | #include "ui/aura/window_tree_host.h" | 
 | #include "ui/base/ime/composition_text.h" | 
 | #include "ui/base/ime/input_method_observer.h" | 
 | #include "ui/events/keycodes/dom/dom_code.h" | 
 | #include "ui/views/widget/widget.h" | 
 |  | 
 | using testing::_; | 
 |  | 
 | namespace exo { | 
 |  | 
 | namespace { | 
 |  | 
 | class MockTextInputDelegate : public TextInput::Delegate { | 
 |  public: | 
 |   MockTextInputDelegate() = default; | 
 |  | 
 |   // TextInput::Delegate: | 
 |   MOCK_METHOD0(Activated, void()); | 
 |   MOCK_METHOD0(Deactivated, void()); | 
 |   MOCK_METHOD1(OnVirtualKeyboardVisibilityChanged, void(bool)); | 
 |   MOCK_METHOD1(SetCompositionText, void(const ui::CompositionText&)); | 
 |   MOCK_METHOD1(Commit, void(const base::string16&)); | 
 |   MOCK_METHOD1(SetCursor, void(const gfx::Range&)); | 
 |   MOCK_METHOD1(DeleteSurroundingText, void(const gfx::Range&)); | 
 |   MOCK_METHOD1(SendKey, void(const ui::KeyEvent&)); | 
 |   MOCK_METHOD1(OnLanguageChanged, void(const std::string&)); | 
 |   MOCK_METHOD1(OnTextDirectionChanged, | 
 |                void(base::i18n::TextDirection direction)); | 
 |  | 
 |  private: | 
 |   DISALLOW_COPY_AND_ASSIGN(MockTextInputDelegate); | 
 | }; | 
 |  | 
 | class TestingInputMethodObserver : public ui::InputMethodObserver { | 
 |  public: | 
 |   explicit TestingInputMethodObserver(ui::InputMethod* input_method) | 
 |       : input_method_(input_method) { | 
 |     input_method_->AddObserver(this); | 
 |   } | 
 |  | 
 |   ~TestingInputMethodObserver() override { | 
 |     input_method_->RemoveObserver(this); | 
 |   } | 
 |  | 
 |   // ui::InputMethodObserver | 
 |   MOCK_METHOD0(OnFocus, void()); | 
 |   MOCK_METHOD0(OnBlur, void()); | 
 |   MOCK_METHOD1(OnCaretBoundsChanged, void(const ui::TextInputClient*)); | 
 |   MOCK_METHOD1(OnTextInputStateChanged, void(const ui::TextInputClient*)); | 
 |   MOCK_METHOD1(OnInputMethodDestroyed, void(const ui::InputMethod*)); | 
 |   MOCK_METHOD0(OnShowVirtualKeyboardIfEnabled, void()); | 
 |  | 
 |  private: | 
 |   ui::InputMethod* input_method_ = nullptr; | 
 |  | 
 |   DISALLOW_COPY_AND_ASSIGN(TestingInputMethodObserver); | 
 | }; | 
 |  | 
 | class TextInputTest : public test::ExoTestBase { | 
 |  public: | 
 |   TextInputTest() = default; | 
 |  | 
 |   void SetUp() override { | 
 |     test::ExoTestBase::SetUp(); | 
 |     text_input_ = | 
 |         std::make_unique<TextInput>(std::make_unique<MockTextInputDelegate>()); | 
 |     SetupSurface(); | 
 |   } | 
 |  | 
 |   void TearDown() override { | 
 |     TearDownSurface(); | 
 |     text_input_.reset(); | 
 |     test::ExoTestBase::TearDown(); | 
 |   } | 
 |  | 
 |   void SetupSurface() { | 
 |     gfx::Size buffer_size(32, 32); | 
 |     buffer_ = std::make_unique<Buffer>( | 
 |         exo_test_helper()->CreateGpuMemoryBuffer(buffer_size)); | 
 |     surface_ = std::make_unique<Surface>(); | 
 |     shell_surface_ = std::make_unique<ShellSurface>(surface_.get()); | 
 |  | 
 |     surface_->Attach(buffer_.get()); | 
 |     surface_->Commit(); | 
 |  | 
 |     gfx::Point origin(100, 100); | 
 |     shell_surface_->SetGeometry(gfx::Rect(origin, buffer_size)); | 
 |   } | 
 |  | 
 |   void TearDownSurface() { | 
 |     shell_surface_.reset(); | 
 |     surface_.reset(); | 
 |     buffer_.reset(); | 
 |   } | 
 |  | 
 |  protected: | 
 |   TextInput* text_input() { return text_input_.get(); } | 
 |   MockTextInputDelegate* delegate() { | 
 |     return static_cast<MockTextInputDelegate*>(text_input_->delegate()); | 
 |   } | 
 |   Surface* surface() { return surface_.get(); } | 
 |  | 
 |   ui::InputMethod* GetInputMethod() { | 
 |     return surface_->window()->GetHost()->GetInputMethod(); | 
 |   } | 
 |  | 
 |   void SetCompositionText(const std::string& utf8) { | 
 |     ui::CompositionText t; | 
 |     t.text = base::UTF8ToUTF16(utf8); | 
 |     t.selection = gfx::Range(1u); | 
 |     t.ime_text_spans.push_back( | 
 |         ui::ImeTextSpan(ui::ImeTextSpan::Type::kComposition, 0, t.text.size(), | 
 |                         ui::ImeTextSpan::Thickness::kThick)); | 
 |     EXPECT_CALL(*delegate(), SetCompositionText(t)).Times(1); | 
 |     text_input()->SetCompositionText(t); | 
 |   } | 
 |  | 
 |  private: | 
 |   std::unique_ptr<TextInput> text_input_; | 
 |  | 
 |   std::unique_ptr<Buffer> buffer_; | 
 |   std::unique_ptr<Surface> surface_; | 
 |   std::unique_ptr<ShellSurface> shell_surface_; | 
 |  | 
 |   DISALLOW_COPY_AND_ASSIGN(TextInputTest); | 
 | }; | 
 |  | 
 | TEST_F(TextInputTest, Activate) { | 
 |   EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, text_input()->GetTextInputType()); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_MODE_DEFAULT, text_input()->GetTextInputMode()); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Activated).Times(1); | 
 |   text_input()->Activate(surface()); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, text_input()->GetTextInputType()); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_MODE_TEXT, text_input()->GetTextInputMode()); | 
 |   EXPECT_EQ(0, text_input()->GetTextInputFlags()); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Deactivated).Times(1); | 
 |   text_input()->Deactivate(); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, text_input()->GetTextInputType()); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_MODE_DEFAULT, text_input()->GetTextInputMode()); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, ShowVirtualKeyboardIfEnabled) { | 
 |   TestingInputMethodObserver observer(GetInputMethod()); | 
 |  | 
 |   EXPECT_CALL(observer, OnTextInputStateChanged(text_input())).Times(1); | 
 |   EXPECT_CALL(*delegate(), Activated).Times(1); | 
 |   text_input()->Activate(surface()); | 
 |  | 
 |   EXPECT_CALL(observer, OnShowVirtualKeyboardIfEnabled) | 
 |       .WillOnce(testing::Invoke( | 
 |           [this]() { text_input()->OnKeyboardVisibilityChanged(true); })); | 
 |   EXPECT_CALL(*delegate(), OnVirtualKeyboardVisibilityChanged(true)).Times(1); | 
 |   text_input()->ShowVirtualKeyboardIfEnabled(); | 
 |  | 
 |   EXPECT_CALL(observer, OnTextInputStateChanged(nullptr)).Times(1); | 
 |   EXPECT_CALL(*delegate(), Deactivated).Times(1); | 
 |   text_input()->Deactivate(); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, ShowVirtualKeyboardIfEnabledBeforeActivated) { | 
 |   TestingInputMethodObserver observer(GetInputMethod()); | 
 |  | 
 |   // ShowVirtualKeyboardIfEnabled before activation. | 
 |   text_input()->ShowVirtualKeyboardIfEnabled(); | 
 |  | 
 |   EXPECT_CALL(observer, OnTextInputStateChanged(text_input())).Times(1); | 
 |   EXPECT_CALL(observer, OnShowVirtualKeyboardIfEnabled) | 
 |       .WillOnce(testing::Invoke( | 
 |           [this]() { text_input()->OnKeyboardVisibilityChanged(true); })); | 
 |   EXPECT_CALL(*delegate(), Activated).Times(1); | 
 |   EXPECT_CALL(*delegate(), OnVirtualKeyboardVisibilityChanged(true)).Times(1); | 
 |   text_input()->Activate(surface()); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Deactivated).Times(1); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, SetTypeModeFlag) { | 
 |   TestingInputMethodObserver observer(GetInputMethod()); | 
 |  | 
 |   EXPECT_CALL(observer, OnTextInputStateChanged(text_input())).Times(1); | 
 |   EXPECT_CALL(*delegate(), Activated).Times(1); | 
 |   text_input()->Activate(surface()); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_TYPE_TEXT, text_input()->GetTextInputType()); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_MODE_TEXT, text_input()->GetTextInputMode()); | 
 |   EXPECT_EQ(0, text_input()->GetTextInputFlags()); | 
 |   EXPECT_TRUE(text_input()->ShouldDoLearning()); | 
 |  | 
 |   int flags = ui::TEXT_INPUT_FLAG_AUTOCOMPLETE_OFF | | 
 |               ui::TEXT_INPUT_FLAG_AUTOCAPITALIZE_NONE; | 
 |   EXPECT_CALL(observer, OnTextInputStateChanged(text_input())).Times(1); | 
 |   text_input()->SetTypeModeFlags(ui::TEXT_INPUT_TYPE_URL, | 
 |                                  ui::TEXT_INPUT_MODE_URL, flags, false); | 
 |  | 
 |   EXPECT_EQ(ui::TEXT_INPUT_TYPE_URL, text_input()->GetTextInputType()); | 
 |   EXPECT_EQ(ui::TEXT_INPUT_MODE_URL, text_input()->GetTextInputMode()); | 
 |   EXPECT_EQ(flags, text_input()->GetTextInputFlags()); | 
 |   EXPECT_FALSE(text_input()->ShouldDoLearning()); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Deactivated).Times(1); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, CaretBounds) { | 
 |   TestingInputMethodObserver observer(GetInputMethod()); | 
 |  | 
 |   EXPECT_CALL(observer, OnTextInputStateChanged(text_input())).Times(1); | 
 |   EXPECT_CALL(*delegate(), Activated).Times(1); | 
 |   text_input()->Activate(surface()); | 
 |  | 
 |   gfx::Rect bounds(10, 10, 0, 16); | 
 |   EXPECT_CALL(observer, OnCaretBoundsChanged(text_input())).Times(1); | 
 |   text_input()->SetCaretBounds(bounds); | 
 |  | 
 |   EXPECT_EQ(bounds.size().ToString(), | 
 |             text_input()->GetCaretBounds().size().ToString()); | 
 |   gfx::Point origin = surface()->window()->GetBoundsInScreen().origin(); | 
 |   origin += bounds.OffsetFromOrigin(); | 
 |   EXPECT_EQ(origin.ToString(), | 
 |             text_input()->GetCaretBounds().origin().ToString()); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Deactivated).Times(1); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, CompositionText) { | 
 |   SetCompositionText("composition"); | 
 |  | 
 |   ui::CompositionText empty; | 
 |   EXPECT_CALL(*delegate(), SetCompositionText(empty)).Times(1); | 
 |   text_input()->ClearCompositionText(); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, CommitCompositionText) { | 
 |   SetCompositionText("composition"); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Commit(base::UTF8ToUTF16("composition"))).Times(1); | 
 |   text_input()->ConfirmCompositionText(/** keep_selection */ false); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, Commit) { | 
 |   base::string16 s = base::ASCIIToUTF16("commit text"); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Commit(s)).Times(1); | 
 |   text_input()->InsertText(s); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, InsertChar) { | 
 |   ui::KeyEvent ev(ui::ET_KEY_PRESSED, ui::VKEY_RETURN, 0); | 
 |  | 
 |   EXPECT_CALL(*delegate(), SendKey(testing::Ref(ev))).Times(1); | 
 |   text_input()->InsertChar(ev); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, InsertCharNormalKey) { | 
 |   base::char16 ch = 'x'; | 
 |   ui::KeyEvent ev(ch, ui::VKEY_X, ui::DomCode::NONE, 0); | 
 |  | 
 |   EXPECT_CALL(*delegate(), Commit(base::string16(1, ch))).Times(1); | 
 |   EXPECT_CALL(*delegate(), SendKey(_)).Times(0); | 
 |   text_input()->InsertChar(ev); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, SurroundingText) { | 
 |   gfx::Range range; | 
 |   EXPECT_FALSE(text_input()->GetTextRange(&range)); | 
 |   EXPECT_FALSE(text_input()->GetCompositionTextRange(&range)); | 
 |   EXPECT_FALSE(text_input()->GetEditableSelectionRange(&range)); | 
 |   base::string16 got_text; | 
 |   EXPECT_FALSE(text_input()->GetTextFromRange(gfx::Range(0, 1), &got_text)); | 
 |  | 
 |   base::string16 text = base::UTF8ToUTF16("surrounding\xE3\x80\x80text"); | 
 |   text_input()->SetSurroundingText(text, 11, 12); | 
 |  | 
 |   EXPECT_TRUE(text_input()->GetTextRange(&range)); | 
 |   EXPECT_EQ(gfx::Range(0, text.size()).ToString(), range.ToString()); | 
 |  | 
 |   EXPECT_FALSE(text_input()->GetCompositionTextRange(&range)); | 
 |   EXPECT_TRUE(text_input()->GetEditableSelectionRange(&range)); | 
 |   EXPECT_EQ(gfx::Range(11, 12).ToString(), range.ToString()); | 
 |   EXPECT_TRUE(text_input()->GetTextFromRange(gfx::Range(11, 12), &got_text)); | 
 |   EXPECT_EQ(text.substr(11, 1), got_text); | 
 |  | 
 |   // DeleteSurroundingText receives the range in UTF8 -- so (11, 14) range is | 
 |   // expected. | 
 |   EXPECT_CALL(*delegate(), DeleteSurroundingText(gfx::Range(11, 14))).Times(1); | 
 |   text_input()->ExtendSelectionAndDelete(0, 0); | 
 |  | 
 |   size_t composition_size = std::string("composition").size(); | 
 |   SetCompositionText("composition"); | 
 |   EXPECT_TRUE(text_input()->GetCompositionTextRange(&range)); | 
 |   EXPECT_EQ(gfx::Range(11, 11 + composition_size).ToString(), range.ToString()); | 
 |   EXPECT_TRUE(text_input()->GetTextRange(&range)); | 
 |   EXPECT_EQ(gfx::Range(0, text.size() - 1 + composition_size).ToString(), | 
 |             range.ToString()); | 
 |   EXPECT_TRUE(text_input()->GetEditableSelectionRange(&range)); | 
 |   EXPECT_EQ(gfx::Range(11, 12).ToString(), range.ToString()); | 
 | } | 
 |  | 
 | TEST_F(TextInputTest, GetTextRange) { | 
 |   base::string16 text = base::UTF8ToUTF16("surrounding text"); | 
 |   text_input()->SetSurroundingText(text, 11, 12); | 
 |  | 
 |   SetCompositionText("composition"); | 
 |  | 
 |   const struct { | 
 |     gfx::Range range; | 
 |     std::string expected; | 
 |   } kTestCases[] = { | 
 |       {gfx::Range(0, 3), "sur"}, | 
 |       {gfx::Range(10, 13), "gco"}, | 
 |       {gfx::Range(10, 23), "gcompositiont"}, | 
 |       {gfx::Range(12, 15), "omp"}, | 
 |       {gfx::Range(12, 23), "ompositiont"}, | 
 |       {gfx::Range(22, 25), "tex"}, | 
 |   }; | 
 |   for (auto& c : kTestCases) { | 
 |     base::string16 result; | 
 |     EXPECT_TRUE(text_input()->GetTextFromRange(c.range, &result)) | 
 |         << c.range.ToString(); | 
 |     EXPECT_EQ(base::UTF8ToUTF16(c.expected), result) << c.range.ToString(); | 
 |   } | 
 | } | 
 |  | 
 | }  // anonymous namespace | 
 | }  // namespace exo |