// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifdef UNSAFE_BUFFERS_BUILD // TODO(crbug.com/40285824): Remove this and convert code to safer constructs. #pragma allow_unsafe_buffers #endif #include <memory> #include <vector> #include "base/command_line.h" #include "base/functional/bind.h" #include "base/memory/raw_ptr.h" #include "base/run_loop.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "build/chromeos_buildflags.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/location_bar/location_bar.h" #include "chrome/browser/ui/tabs/tab_enums.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/test/base/in_process_browser_test.h" #include "chrome/test/base/interactive_test_utils.h" #include "chrome/test/base/ui_test_utils.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "content/public/common/content_client.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test_utils.h" #include "content/public/test/no_renderer_crashes_assertion.h" #include "content/public/test/test_utils.h" #include "content/public/test/text_input_test_utils.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "third_party/blink/public/common/switches.h" #include "ui/base/ime/ime_text_span.h" #include "ui/base/ime/text_edit_commands.h" #include "ui/base/ime/text_input_client.h" #include "ui/base/ime/text_input_mode.h" #include "ui/base/ime/text_input_type.h" #include "url/gurl.h" #if BUILDFLAG(IS_LINUX) #include "ui/base/ime/linux/text_edit_command_auralinux.h" #include "ui/linux/fake_linux_ui.h" #include "ui/linux/linux_ui.h" #endif /////////////////////////////////////////////////////////////////////////////// // TextInputManager and IME Tests // // The following tests verify the correctness of TextInputState tracking on the // browser side. They also make sure the IME logic works correctly. The baseline // for comparison is the default functionality in the non-OOPIF case (i.e., the // legacy implementation in RWHV's other than RWHVCF). // These tests live outside content/ because they rely on being part of the // interactive UI test framework (to avoid flakiness). namespace { IndexVector; // This class observes TextInputManager for the first change in TextInputState. class TextInputManagerChangeObserver : public content::TextInputManagerObserverBase { … }; // This class observes |TextInputState.type| for a specific RWHV. class ViewTextInputTypeObserver : public content::TextInputManagerObserverBase { … }; // This class observes the |expected_view| for the first change in its // selection bounds. class ViewSelectionBoundsChangedObserver : public content::TextInputManagerObserverBase { … }; // This class observes the |expected_view| for the first change in its // composition range information. class ViewCompositionRangeChangedObserver : public content::TextInputManagerObserverBase { … }; // This class observes the |expected_view| for a change in the text selection. class ViewTextSelectionObserver : public content::TextInputManagerObserverBase { … }; // This class observes all the text selection updates within a WebContents. class TextSelectionObserver : public content::TextInputManagerObserverBase { … }; // This class monitors all the changes in TextInputState and keeps a record of // the active views. There is no waiting and the recording process is // continuous. class RecordActiveViewsObserver { … }; } // namespace // Main class for all TextInputState and IME related tests. class SitePerProcessTextInputManagerTest : public InProcessBrowserTest { … }; // The following test loads a page with multiple nested <iframe> elements which // are in or out of process with the main frame. Then an <input> field with // unique value is added to every single frame on the frame tree. The test then // creates a sequence of tab presses and verifies that after each key press, the // TextInputState.value reflects that of the focused input, i.e., the // TextInputManager is correctly tracking TextInputState across frames. // Flaky on ChromeOS, Linux, Mac, and Windows; https://crbug.com/704994. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, DISABLED_TrackStateWhenSwitchingFocusedFrames) { … } // The following test loads a page with two OOPIFs. An <input> is added to both // frames and tab key is pressed until the one in the second OOPIF is focused. // Then, the renderer processes for both frames are crashed. The test verifies // that the TextInputManager stops tracking the RWHVs as well as properly // resets the TextInputState after the second (active) RWHV goes away. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, StopTrackingCrashedChildFrame) { … } // The following test loads a page with two child frames: one in process and one // out of process with main frame. The test inserts an <input> inside each frame // and focuses the first frame and observes the TextInputManager setting the // state to ui::TEXT_INPUT_TYPE_TEXT. Then, the frame is detached and the test // observes that the state type is reset to ui::TEXT_INPUT_TYPE_NONE. The same // sequence of actions is then performed on the out of process frame. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, ResetStateAfterFrameDetached) { … } // This test creates a page with one OOPIF and adds an <input> to it. Then, the // <input> is focused and the test verfies that the |TextInputState.type| is set // to ui::TEXT_INPUT_TYPE_TEXT. Next, the child frame is navigated away and the // test verifies that |TextInputState.type| resets to ui::TEXT_INPUT_TYPE_NONE. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, ResetStateAfterChildNavigation) { … } // This test creates a blank page and adds an <input> to it. Then, the <input> // is focused and the test verfies that the |TextInputState.type| is set to // ui::TEXT_INPUT_TYPE_TEXT. Next, the browser is navigated away and the test // verifies that |TextInputState.type| resets to ui::TEXT_INPUT_TYPE_NONE. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, ResetStateAfterBrowserNavigation) { … } #if defined(USE_AURA) // This test creates a blank page and adds an <input> to it. Then, the <input> // is focused, UI is focused, then the input is refocused. The test verifies // that selection bounds change with the refocus (see https://crbug.com/864563). IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, SelectionBoundsChangeAfterRefocusInput) { … } #endif // This test verifies that if we have a focused <input> in the main frame and // the tab is closed, TextInputManager handles unregistering itself and // notifying the observers properly (see https://crbug.com/669375). IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, ClosingTabWillNotCrash) { … } // The following test verifies that when the active widget changes value, it is // always from nullptr to non-null or vice versa. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, ResetTextInputStateOnActiveWidgetChange) { … } // This test creates a page with multiple child frames and adds an <input> to // each frame. Then, sequentially, each <input> is focused by sending a tab key. // Then, after |TextInputState.type| for a view is changed to text, the test // sends a set composition IPC to the active widget and waits until the widget // updates its composition range. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, TrackCompositionRangeForAllFrames) { … } // Failing on Mac - http://crbug.com/852452 #if BUILDFLAG(IS_MAC) #define MAYBE_TrackTextSelectionForAllFrames … #else #define MAYBE_TrackTextSelectionForAllFrames … #endif // This test creates a page with multiple child frames and adds an <input> to // each frame. Then, sequentially, each <input> is focused by sending a tab key. // After focusing each input, a sequence of key presses (character 'E') are sent // to the focused widget. The test then verifies that the selection length // equals the length of the sequence of 'E's. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, MAYBE_TrackTextSelectionForAllFrames) { … } // This test verifies that committing text works as expected for all the frames // on the page. Specifically, the test sends an IPC to the RenderWidget // corresponding to a focused frame with a focused <input> to commit some text. // Then, it verifies that the <input>'s value matches the committed text // (https://crbug.com/688842). // Flaky on Android and Linux http://crbug.com/852274 #if BUILDFLAG(IS_MAC) #define MAYBE_ImeCommitTextForAllFrames … #else #define MAYBE_ImeCommitTextForAllFrames … #endif IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, MAYBE_ImeCommitTextForAllFrames) { … } // TODO(ekaramad): Some of the following tests should be active on Android as // well. Enable them when the corresponding feature is implemented for Android // (https://crbug.com/602723). #if !BUILDFLAG(IS_ANDROID) // This test creates a page with multiple child frames and adds an <input> to // each frame. Then, sequentially, each <input> is focused by sending a tab key. // Then, after |TextInputState.type| for a view is changed to text, another key // is pressed (a character) and then the test verifies that TextInputManager // receives the corresponding update on the change in selection bounds on the // browser side. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, TrackSelectionBoundsForAllFrames) { … } // This test makes sure browser correctly tracks focused editable element inside // each RenderFrameHost. // Test is flaky on chromeOS; https://crbug.com/705203. #if BUILDFLAG(IS_CHROMEOS_ASH) #define MAYBE_TrackingFocusedElementForAllFrames … #else #define MAYBE_TrackingFocusedElementForAllFrames … #endif IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, MAYBE_TrackingFocusedElementForAllFrames) { … } // This test tracks page level focused editable element tracking using // WebContents. In a page with multiple frames, a frame is selected and // focused. Then the <input> inside frame is both focused and blurred and and // in both cases the test verifies that WebContents is aware whether or not a // focused editable element exists on the page. // Test is flaky on ChromeOS. crbug.com/705289 #if BUILDFLAG(IS_CHROMEOS_ASH) #define MAYBE_TrackPageFocusEditableElement … #else #define MAYBE_TrackPageFocusEditableElement … #endif IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, MAYBE_TrackPageFocusEditableElement) { … } // TODO(ekaramad): Could this become a unit test instead? // This test focuses <input> elements on the page and verifies that // WebContents knows about the focused editable element. Then it asks the // WebContents to clear focused element and verifies that there is no longer // a focused editable element on the page. // Test is flaky on ChromeOS; https://crbug.com/705203. #if BUILDFLAG(IS_CHROMEOS_ASH) #define MAYBE_ClearFocusedElementOnPage … #else #define MAYBE_ClearFocusedElementOnPage … #endif IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, MAYBE_ClearFocusedElementOnPage) { … } // TODO(ekaramad): The following tests are specifically written for Aura and are // based on InputMethodObserver. Write similar tests for Mac/Android/Mus // (crbug.com/602723). #if defined(USE_AURA) // ----------------------------------------------------------------------------- // Input Method Observer Tests // // The following tests will make use of the InputMethodObserver to verify that // OOPIF pages interact properly with the InputMethod through the tab's view. // TODO(ekaramad): We only have coverage for some aura tests as the whole idea // of ui::TextInputClient/ui::InputMethod/ui::InputMethodObserver seems to be // only fit to aura (specifically, OS_CHROMEOS). Can we add more tests here for // aura as well as other platforms (https://crbug.com/602723)? // Observes current input method for state changes. class InputMethodObserverBase { … }; class InputMethodObserverForShowIme : public InputMethodObserverBase { … }; // TODO(ekaramad): This test is actually a unit test and should be moved to // somewhere more relevant (https://crbug.com/602723). // This test verifies that the IME for Aura is shown if and only if the current // client's |TextInputState.type| is not ui::TEXT_INPUT_TYPE_NONE and the flag // |TextInputState.show_ime_if_needed| is true. This should happen even when // the TextInputState has not changed (according to the platform), e.g., in // aura when receiving two consecutive updates with same |TextInputState.type|. // This test is disabled on Windows because we have removed TryShow/TryHide API // calls and replaced it with TSF input pane policy which is a policy applied by // text service framework on Windows based on whether TSF edit control has focus // or not. On Windows we have implemented TSF1 on Chromium that takes care of // IME compositions, handwriting panels, SIP visibility etc. Please see // (https://crbug.com/1007958) for more details. #if !BUILDFLAG(IS_WIN) IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, CorrectlyShowVirtualKeyboardIfEnabled) { … } #endif // !BUILDFLAG(IS_WIN) #endif // USE_AURA // Ensure that a cross-process subframe can utilize keyboard edit commands. // See https://crbug.com/640706. This test is Linux-specific, as it relies on // overriding ui::LinuxUi. #if BUILDFLAG(IS_LINUX) IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, SubframeKeyboardEditCommands) { … } #endif // Ideally, the following code + test should be live in // 'site_per_process_mac_browsertest.mm'. However, the test // 'LookUpStringForRangeRoutesToFocusedWidget' relies on an override in // ContentBrowserClient to register its filters in time. In content shell, we // cannot have two instances of ShellContentBrowserClient (due to a DCHECK in // the ctor). Therefore, we put the test here to use ChromeContentBrowserClient // which does not have the same singleton constraint. #if BUILDFLAG(IS_MAC) class ShowDefinitionForWordObserver : content::RenderWidgetHostViewCocoaObserver { public: explicit ShowDefinitionForWordObserver(content::WebContents* web_contents) : content::RenderWidgetHostViewCocoaObserver(web_contents) {} ShowDefinitionForWordObserver(const ShowDefinitionForWordObserver&) = delete; ShowDefinitionForWordObserver& operator=( const ShowDefinitionForWordObserver&) = delete; ~ShowDefinitionForWordObserver() override {} const std::string& WaitForWordLookUp() { if (did_receive_string_) return word_; run_loop_ = std::make_unique<base::RunLoop>(); run_loop_->Run(); return word_; } private: void OnShowDefinitionForAttributedString( const std::string& for_word) override { did_receive_string_ = true; word_ = for_word; if (run_loop_) run_loop_->Quit(); } bool did_receive_string_ = false; std::string word_; std::unique_ptr<base::RunLoop> run_loop_; }; // Flakey (https:://crbug.com/874417). // This test verifies that requests for dictionary lookup based on selection // range are routed to the focused RenderWidgetHost. IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, DISABLED_LookUpStringForRangeRoutesToFocusedWidget) { CreateIframePage("a(b)"); std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{}), GetFrame(IndexVector{0})}; std::string expected_words[] = {"word1", "word2"}; // For each frame, add <input>, set its value to expected word, select it, ask // for dictionary and verify the word returned from renderer matches. for (size_t i = 0; i < frames.size(); ++i) { AddInputFieldToFrame(frames[i], "text", expected_words[i].c_str(), true); // Focusing the <input> automatically selects the text. ASSERT_TRUE(ExecJs(frames[i], "document.querySelector('input').focus();")); ShowDefinitionForWordObserver word_lookup_observer(active_contents()); // Request for the dictionary lookup and intercept the word on its way back. // The request is always on the tab's view which is a // RenderWidgetHostViewMac. content::AskForLookUpDictionaryForRange( active_contents()->GetRenderWidgetHostView(), gfx::Range(0, expected_words[i].size())); EXPECT_EQ(expected_words[i], word_lookup_observer.WaitForWordLookUp()); } } // This test verifies that when a word lookup result comes from the renderer // after the target RenderWidgetHost has been deleted, the browser will not // crash. This test covers the case where the target RenderWidgetHost is that of // an OOPIF. IN_PROC_BROWSER_TEST_F( SitePerProcessTextInputManagerTest, DoNotCrashBrowserInWordLookUpForDestroyedWidget_ChildFrame) { std::unique_ptr<content::WebContents> new_contents = content::WebContents::Create(content::WebContents::CreateParams( active_contents()->GetBrowserContext(), nullptr)); content::WebContents* raw_new_contents = new_contents.get(); browser()->tab_strip_model()->InsertWebContentsAt(1, std::move(new_contents), AddTabTypes::ADD_ACTIVE); EXPECT_EQ(active_contents(), raw_new_contents); // Simple page with 1 cross origin (out-of-process) <iframe>. CreateIframePage("a(b)"); content::RenderFrameHost* child_frame = GetFrame(IndexVector{0}); // Now add an <input> field and select its text. AddInputFieldToFrame(child_frame, "text", "four", true); EXPECT_TRUE(ExecJs(child_frame, "window.focus();" "document.querySelector('input').focus();" "document.querySelector('input').select();")); content::TextInputTestLocalFrame text_input_local_frame; text_input_local_frame.SetUp(child_frame); // We need to wait for test scenario to complete before leaving this block. base::RunLoop test_complete_waiter; // Destroy the RenderWidgetHost from the browser side right after the // dictionary message is received. The destruction is post tasked to UI // thread. int32_t child_process_id = child_frame->GetProcess()->GetID(); int32_t child_frame_routing_id = child_frame->GetRoutingID(); text_input_local_frame.SetStringForRangeCallback(base::BindRepeating( [](int32_t process_id, int32_t routing_id, const base::RepeatingClosure& callback_on_io) { // This runs before TextInputClientMac gets to handle the mojo message. // Then, by the time TextInputClientMac calls back into UI to show the // dictionary, the target RWH is already destroyed which will be a // close enough repro for the crash in https://crbug.com/737032. ASSERT_TRUE(content::DestroyRenderWidgetHost(process_id, routing_id)); // Quit the run loop on IO to make sure the message handler of // TextInputClientMac has successfully run on UI thread. content::GetIOThreadTaskRunner({})->PostTask(FROM_HERE, callback_on_io); }, child_process_id, child_frame_routing_id, test_complete_waiter.QuitClosure())); content::RenderWidgetHostView* page_rwhv = content::WebContents::FromRenderFrameHost(child_frame) ->GetRenderWidgetHostView(); // The dictionary request to be made will be routed to the focused frame. ASSERT_EQ(child_frame, raw_new_contents->GetFocusedFrame()); // Request for the dictionary lookup and intercept the word on its way back. // The request is always on the tab's view which is a RenderWidgetHostViewMac. content::AskForLookUpDictionaryForRange(page_rwhv, gfx::Range(0, 4)); test_complete_waiter.Run(); } // This test verifies that when a word lookup result comes from the renderer // after the target RenderWidgetHost has been deleted, the browser will not // crash. This test covers the case where the target RenderWidgetHost is that of // the main frame (no OOPIFs on page). IN_PROC_BROWSER_TEST_F( SitePerProcessTextInputManagerTest, DoNotCrashBrowserInWordLookUpForDestroyedWidget_MainFrame) { std::unique_ptr<content::WebContents> new_contents = content::WebContents::Create(content::WebContents::CreateParams( active_contents()->GetBrowserContext(), nullptr)); content::WebContents* raw_new_contents = new_contents.get(); browser()->tab_strip_model()->InsertWebContentsAt(1, std::move(new_contents), AddTabTypes::ADD_ACTIVE); EXPECT_EQ(active_contents(), raw_new_contents); // Simple page with no <iframe>s. CreateIframePage("a()"); content::RenderFrameHost* main_frame = GetFrame(IndexVector{}); // Now add an <input> field and select its text. AddInputFieldToFrame(main_frame, "text", "four", true); EXPECT_TRUE(ExecJs(main_frame, "document.querySelector('input').focus();" "document.querySelector('input').select();")); content::TextInputTestLocalFrame text_input_local_frame; text_input_local_frame.SetUp(main_frame); content::RenderWidgetHostView* page_rwhv = main_frame->GetView(); // We need to wait for test scenario to complete before leaving this block. base::RunLoop test_complete_waiter; // Destroy the RenderWidgetHost from the browser side right after the // dictionary message is received. The destruction is post tasked to UI // thread. int32_t main_frame_process_id = main_frame->GetProcess()->GetID(); int32_t main_frame_routing_id = main_frame->GetRoutingID(); text_input_local_frame.SetStringForRangeCallback(base::BindRepeating( [](int32_t process_id, int32_t routing_id, const base::RepeatingClosure& callback_on_io) { // This runs before TextInputClientMac gets to handle the mojo message. // Then, by the time TextInputClientMac calls back into UI to show the // dictionary, the target RWH is already destroyed which will be a // close enough repro for the crash in https://crbug.com/737032. ASSERT_TRUE(content::DestroyRenderWidgetHost(process_id, routing_id)); // Quit the run loop on IO to make sure the message handler of // TextInputClientMac has successfully run on UI thread. content::GetIOThreadTaskRunner({})->PostTask(FROM_HERE, callback_on_io); }, main_frame_process_id, main_frame_routing_id, test_complete_waiter.QuitClosure())); // Request for the dictionary lookup and intercept the word on its way back. // The request is always on the tab's view which is a RenderWidgetHostViewMac. content::AskForLookUpDictionaryForRange(page_rwhv, gfx::Range(0, 4)); test_complete_waiter.Run(); } #endif // defined(MAC_OSX) #endif // !BUILDFLAG(IS_ANDROID)