chromium/chrome/browser/site_isolation/site_per_process_text_input_browsertest.cc

// 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)