|  | // Copyright 2012 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "base/memory/raw_ptr.h" | 
|  |  | 
|  | #import "content/browser/renderer_host/text_input_client_mac.h" | 
|  |  | 
|  | #include <stddef.h> | 
|  | #include <stdint.h> | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/task/single_thread_task_runner.h" | 
|  | #include "base/threading/thread.h" | 
|  | #include "base/time/time.h" | 
|  | #include "content/browser/renderer_host/render_process_host_impl.h" | 
|  | #include "content/browser/renderer_host/render_view_host_impl.h" | 
|  | #include "content/public/browser/render_frame_host.h" | 
|  | #include "content/public/browser/web_contents_observer.h" | 
|  | #include "content/public/test/browser_task_environment.h" | 
|  | #include "content/public/test/fake_local_frame.h" | 
|  | #include "content/public/test/mock_render_process_host.h" | 
|  | #include "content/public/test/test_renderer_host.h" | 
|  | #include "ipc/ipc_test_sink.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  | #include "third_party/abseil-cpp/absl/types/optional.h" | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | namespace { | 
|  | const int64_t kTaskDelayMs = 200; | 
|  |  | 
|  | // Stub out local frame mojo binding. Intercepts calls for text input | 
|  | // and marks the message as received. This class attaches to the first | 
|  | // RenderFrameHostImpl created. | 
|  | class TextInputClientLocalFrame : public content::FakeLocalFrame, | 
|  | public WebContentsObserver { | 
|  | public: | 
|  | explicit TextInputClientLocalFrame(WebContents* web_contents) | 
|  | : WebContentsObserver(web_contents) {} | 
|  |  | 
|  | void RenderFrameCreated(RenderFrameHost* render_frame_host) override { | 
|  | if (!initialized_) { | 
|  | initialized_ = true; | 
|  | Init(render_frame_host->GetRemoteAssociatedInterfaces()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void GetCharacterIndexAtPoint(const gfx::Point& point) override { | 
|  | if (completion_callback_) | 
|  | std::move(completion_callback_).Run(); | 
|  | } | 
|  |  | 
|  | void GetFirstRectForRange(const gfx::Range& range) override { | 
|  | if (completion_callback_) | 
|  | std::move(completion_callback_).Run(); | 
|  | } | 
|  |  | 
|  | void SetCallback(base::OnceClosure callback) { | 
|  | completion_callback_ = std::move(callback); | 
|  | } | 
|  |  | 
|  | private: | 
|  | bool initialized_ = false; | 
|  | base::OnceClosure completion_callback_; | 
|  | }; | 
|  |  | 
|  | // This test does not test the WebKit side of the dictionary system (which | 
|  | // performs the actual data fetching), but rather this just tests that the | 
|  | // service's signaling system works. | 
|  | class TextInputClientMacTest : public content::RenderViewHostTestHarness { | 
|  | public: | 
|  | TextInputClientMacTest() : thread_("TextInputClientMacTestThread") {} | 
|  |  | 
|  | void SetUp() override { | 
|  | content::RenderViewHostTestHarness::SetUp(); | 
|  | local_frame_ = std::make_unique<TextInputClientLocalFrame>(web_contents()); | 
|  | RenderViewHostTester::For(rvh())->CreateTestRenderView(); | 
|  | widget_ = rvh()->GetWidget(); | 
|  | FocusWebContentsOnMainFrame(); | 
|  | } | 
|  |  | 
|  | void TearDown() override { | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | RenderViewHostTestHarness::TearDown(); | 
|  | } | 
|  |  | 
|  | // Accessor for the TextInputClientMac instance. | 
|  | TextInputClientMac* service() { | 
|  | return TextInputClientMac::GetInstance(); | 
|  | } | 
|  |  | 
|  | // Helper method to post a task on the testing thread's MessageLoop after | 
|  | // a short delay. | 
|  | void PostTask(base::Location from_here, base::OnceClosure task) { | 
|  | PostTask(std::move(from_here), std::move(task), | 
|  | base::Milliseconds(kTaskDelayMs)); | 
|  | } | 
|  |  | 
|  | void PostTask(base::Location from_here, | 
|  | base::OnceClosure task, | 
|  | const base::TimeDelta delay) { | 
|  | thread_.task_runner()->PostDelayedTask(std::move(from_here), | 
|  | std::move(task), delay); | 
|  | } | 
|  |  | 
|  | RenderWidgetHost* widget() { return widget_; } | 
|  | TextInputClientLocalFrame* local_frame() { return local_frame_.get(); } | 
|  |  | 
|  | IPC::TestSink& ipc_sink() { | 
|  | return static_cast<MockRenderProcessHost*>(widget()->GetProcess())->sink(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | friend class ScopedTestingThread; | 
|  |  | 
|  | raw_ptr<RenderWidgetHost> widget_; | 
|  | std::unique_ptr<TextInputClientLocalFrame> local_frame_; | 
|  |  | 
|  | base::Thread thread_; | 
|  | }; | 
|  |  | 
|  | //////////////////////////////////////////////////////////////////////////////// | 
|  |  | 
|  | // Helper class that Start()s and Stop()s a thread according to the scope of the | 
|  | // object. | 
|  | class ScopedTestingThread { | 
|  | public: | 
|  | ScopedTestingThread(TextInputClientMacTest* test) : thread_(test->thread_) { | 
|  | thread_.Start(); | 
|  | } | 
|  | ~ScopedTestingThread() { | 
|  | thread_.Stop(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | base::Thread& thread_; | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | // Test Cases ////////////////////////////////////////////////////////////////// | 
|  |  | 
|  | TEST_F(TextInputClientMacTest, GetCharacterIndex) { | 
|  | ScopedTestingThread thread(this); | 
|  | const NSUInteger kSuccessValue = 42; | 
|  |  | 
|  | PostTask(FROM_HERE, | 
|  | base::BindOnce(&TextInputClientMac::SetCharacterIndexAndSignal, | 
|  | base::Unretained(service()), kSuccessValue)); | 
|  | base::RunLoop run_loop; | 
|  | local_frame()->SetCallback(run_loop.QuitClosure()); | 
|  | NSUInteger index = service()->GetCharacterIndexAtPoint( | 
|  | widget(), gfx::Point(2, 2)); | 
|  |  | 
|  | EXPECT_EQ(kSuccessValue, index); | 
|  | run_loop.Run(); | 
|  | } | 
|  |  | 
|  | TEST_F(TextInputClientMacTest, TimeoutCharacterIndex) { | 
|  | base::RunLoop run_loop; | 
|  | local_frame()->SetCallback(run_loop.QuitClosure()); | 
|  | uint32_t index = | 
|  | service()->GetCharacterIndexAtPoint(widget(), gfx::Point(2, 2)); | 
|  |  | 
|  | EXPECT_EQ(UINT32_MAX, index); | 
|  | run_loop.Run(); | 
|  | } | 
|  |  | 
|  | TEST_F(TextInputClientMacTest, NotFoundCharacterIndex) { | 
|  | ScopedTestingThread thread(this); | 
|  | const NSUInteger kPreviousValue = 42; | 
|  |  | 
|  | // Set an arbitrary value to ensure the index is not |NSNotFound|. | 
|  | PostTask(FROM_HERE, | 
|  | base::BindOnce(&TextInputClientMac::SetCharacterIndexAndSignal, | 
|  | base::Unretained(service()), kPreviousValue)); | 
|  |  | 
|  | // Set UINT32_MAX to the index |kTaskDelayMs| after the previous setting. | 
|  | PostTask(FROM_HERE, | 
|  | base::BindOnce(&TextInputClientMac::SetCharacterIndexAndSignal, | 
|  | base::Unretained(service()), UINT32_MAX), | 
|  | base::Milliseconds(kTaskDelayMs) * 2); | 
|  |  | 
|  | base::RunLoop run_loop1; | 
|  | local_frame()->SetCallback(run_loop1.QuitClosure()); | 
|  | uint32_t index = | 
|  | service()->GetCharacterIndexAtPoint(widget(), gfx::Point(2, 2)); | 
|  | run_loop1.Run(); | 
|  | EXPECT_EQ(kPreviousValue, index); | 
|  |  | 
|  | base::RunLoop run_loop2; | 
|  | local_frame()->SetCallback(run_loop2.QuitClosure()); | 
|  | index = service()->GetCharacterIndexAtPoint(widget(), gfx::Point(2, 2)); | 
|  | run_loop2.Run(); | 
|  | EXPECT_EQ(UINT32_MAX, index); | 
|  | } | 
|  |  | 
|  | TEST_F(TextInputClientMacTest, GetRectForRange) { | 
|  | ScopedTestingThread thread(this); | 
|  | const gfx::Rect kSuccessValue(42, 43, 44, 45); | 
|  |  | 
|  | PostTask(FROM_HERE, | 
|  | base::BindOnce(&TextInputClientMac::SetFirstRectAndSignal, | 
|  | base::Unretained(service()), kSuccessValue)); | 
|  | base::RunLoop run_loop; | 
|  | local_frame()->SetCallback(run_loop.QuitClosure()); | 
|  | gfx::Rect rect = | 
|  | service()->GetFirstRectForRange(widget(), gfx::Range(NSMakeRange(0, 32))); | 
|  | run_loop.Run(); | 
|  | EXPECT_EQ(kSuccessValue, rect); | 
|  | } | 
|  |  | 
|  | TEST_F(TextInputClientMacTest, TimeoutRectForRange) { | 
|  | base::RunLoop run_loop; | 
|  | local_frame()->SetCallback(run_loop.QuitClosure()); | 
|  |  | 
|  | base::TimeDelta old_timeout = service()->wait_timeout_for_tests(); | 
|  | service()->set_wait_timeout_for_tests(base::Milliseconds(300)); | 
|  |  | 
|  | gfx::Rect rect = | 
|  | service()->GetFirstRectForRange(widget(), gfx::Range(NSMakeRange(0, 32))); | 
|  | run_loop.Run(); | 
|  |  | 
|  | service()->set_wait_timeout_for_tests(old_timeout); | 
|  | EXPECT_EQ(gfx::Rect(), rect); | 
|  | } | 
|  |  | 
|  | }  // namespace content |