blob: af3be2b3ff4683d42b560f740079138ddaf1ec9a [file] [log] [blame]
// Copyright 2016 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 <vector>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "build/build_config.h"
#include "chrome/browser/chrome_content_browser_client.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_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_utils.h"
#include "content/public/test/content_browser_test_utils.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 "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 defined(OS_LINUX) && !defined(OS_CHROMEOS)
#include "ui/base/ime/linux/text_edit_command_auralinux.h"
#include "ui/base/ime/linux/text_edit_key_bindings_delegate_auralinux.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 {
using IndexVector = std::vector<size_t>;
// TextInputManager Observers
// A base class for observing the TextInputManager owned by the given
// WebContents. Subclasses could observe the TextInputManager for different
// changes. The class wraps a public tester which accepts callbacks that
// are run after specific changes in TextInputManager. Different observers can
// be subclassed from this by providing their specific callback methods.
class TextInputManagerObserverBase {
public:
explicit TextInputManagerObserverBase(content::WebContents* web_contents)
: tester_(new content::TextInputManagerTester(web_contents)),
success_(false) {}
virtual ~TextInputManagerObserverBase() {}
// Wait for derived class's definition of success.
void Wait() {
if (success_)
return;
message_loop_runner_ = new content::MessageLoopRunner();
message_loop_runner_->Run();
}
bool success() const { return success_; }
protected:
content::TextInputManagerTester* tester() { return tester_.get(); }
void OnSuccess() {
success_ = true;
if (message_loop_runner_)
message_loop_runner_->Quit();
// By deleting |tester_| we make sure that the internal observer used in
// content/ is removed from the observer list of TextInputManager.
tester_.reset(nullptr);
}
private:
std::unique_ptr<content::TextInputManagerTester> tester_;
bool success_;
scoped_refptr<content::MessageLoopRunner> message_loop_runner_;
DISALLOW_COPY_AND_ASSIGN(TextInputManagerObserverBase);
};
// This class observes TextInputManager for changes in |TextInputState.value|.
class TextInputManagerValueObserver : public TextInputManagerObserverBase {
public:
TextInputManagerValueObserver(content::WebContents* web_contents,
const std::string& expected_value)
: TextInputManagerObserverBase(web_contents),
expected_value_(expected_value) {
tester()->SetUpdateTextInputStateCalledCallback(base::Bind(
&TextInputManagerValueObserver::VerifyValue, base::Unretained(this)));
}
private:
void VerifyValue() {
std::string value;
if (tester()->GetTextInputValue(&value) && expected_value_ == value)
OnSuccess();
}
const std::string expected_value_;
DISALLOW_COPY_AND_ASSIGN(TextInputManagerValueObserver);
};
// This class observes TextInputManager for changes in |TextInputState.type|.
class TextInputManagerTypeObserver : public TextInputManagerObserverBase {
public:
TextInputManagerTypeObserver(content::WebContents* web_contents,
ui::TextInputType expected_type)
: TextInputManagerObserverBase(web_contents),
expected_type_(expected_type) {
tester()->SetUpdateTextInputStateCalledCallback(base::Bind(
&TextInputManagerTypeObserver::VerifyType, base::Unretained(this)));
}
private:
void VerifyType() {
ui::TextInputType type =
tester()->GetTextInputType(&type) ? type : ui::TEXT_INPUT_TYPE_NONE;
if (expected_type_ == type)
OnSuccess();
}
const ui::TextInputType expected_type_;
DISALLOW_COPY_AND_ASSIGN(TextInputManagerTypeObserver);
};
// This class observes TextInputManager for the first change in TextInputState.
class TextInputManagerChangeObserver : public TextInputManagerObserverBase {
public:
explicit TextInputManagerChangeObserver(content::WebContents* web_contents)
: TextInputManagerObserverBase(web_contents) {
tester()->SetUpdateTextInputStateCalledCallback(base::Bind(
&TextInputManagerChangeObserver::VerifyChange, base::Unretained(this)));
}
private:
void VerifyChange() {
if (tester()->IsTextInputStateChanged())
OnSuccess();
}
DISALLOW_COPY_AND_ASSIGN(TextInputManagerChangeObserver);
};
// This class observes |TextInputState.type| for a specific RWHV.
class ViewTextInputTypeObserver : public TextInputManagerObserverBase {
public:
explicit ViewTextInputTypeObserver(content::WebContents* web_contents,
content::RenderWidgetHostView* rwhv,
ui::TextInputType expected_type)
: TextInputManagerObserverBase(web_contents),
web_contents_(web_contents),
view_(rwhv),
expected_type_(expected_type) {
tester()->SetUpdateTextInputStateCalledCallback(base::Bind(
&ViewTextInputTypeObserver::VerifyType, base::Unretained(this)));
}
private:
void VerifyType() {
ui::TextInputType type;
if (!content::GetTextInputTypeForView(web_contents_, view_, &type))
return;
if (expected_type_ == type)
OnSuccess();
}
content::WebContents* web_contents_;
content::RenderWidgetHostView* view_;
const ui::TextInputType expected_type_;
DISALLOW_COPY_AND_ASSIGN(ViewTextInputTypeObserver);
};
// This class observes the |expected_view| for the first change in its
// selection bounds.
class ViewSelectionBoundsChangedObserver : public TextInputManagerObserverBase {
public:
ViewSelectionBoundsChangedObserver(
content::WebContents* web_contents,
content::RenderWidgetHostView* expected_view)
: TextInputManagerObserverBase(web_contents),
expected_view_(expected_view) {
tester()->SetOnSelectionBoundsChangedCallback(
base::Bind(&ViewSelectionBoundsChangedObserver::VerifyChange,
base::Unretained(this)));
}
private:
void VerifyChange() {
if (expected_view_ == tester()->GetUpdatedView())
OnSuccess();
}
const content::RenderWidgetHostView* const expected_view_;
DISALLOW_COPY_AND_ASSIGN(ViewSelectionBoundsChangedObserver);
};
// This class observes the |expected_view| for the first change in its
// composition range information.
class ViewCompositionRangeChangedObserver
: public TextInputManagerObserverBase {
public:
ViewCompositionRangeChangedObserver(
content::WebContents* web_contents,
content::RenderWidgetHostView* expected_view)
: TextInputManagerObserverBase(web_contents),
expected_view_(expected_view) {
tester()->SetOnImeCompositionRangeChangedCallback(
base::Bind(&ViewCompositionRangeChangedObserver::VerifyChange,
base::Unretained(this)));
}
private:
void VerifyChange() {
if (expected_view_ == tester()->GetUpdatedView())
OnSuccess();
}
const content::RenderWidgetHostView* const expected_view_;
DISALLOW_COPY_AND_ASSIGN(ViewCompositionRangeChangedObserver);
};
// This class observes the |expected_view| for a change in the text selection.
class ViewTextSelectionObserver : public TextInputManagerObserverBase {
public:
ViewTextSelectionObserver(content::WebContents* web_contents,
content::RenderWidgetHostView* expected_view,
size_t expected_length)
: TextInputManagerObserverBase(web_contents),
expected_view_(expected_view),
expected_length_(expected_length) {
tester()->SetOnTextSelectionChangedCallback(base::Bind(
&ViewTextSelectionObserver::VerifyChange, base::Unretained(this)));
}
private:
void VerifyChange() {
if (expected_view_ == tester()->GetUpdatedView()) {
size_t length;
EXPECT_TRUE(tester()->GetCurrentTextSelectionLength(&length));
if (length == expected_length_)
OnSuccess();
}
}
const content::RenderWidgetHostView* const expected_view_;
const size_t expected_length_;
DISALLOW_COPY_AND_ASSIGN(ViewTextSelectionObserver);
};
// This class observes all the text selection updates within a WebContents.
class TextSelectionObserver : public TextInputManagerObserverBase {
public:
explicit TextSelectionObserver(content::WebContents* web_contents)
: TextInputManagerObserverBase(web_contents) {
tester()->SetOnTextSelectionChangedCallback(base::Bind(
&TextSelectionObserver::VerifyChange, base::Unretained(this)));
}
void WaitForSelectedText(const std::string& text) {
selected_text_ = text;
Wait();
}
private:
void VerifyChange() {
if (base::UTF16ToUTF8(tester()->GetUpdatedView()->GetSelectedText()) ==
selected_text_) {
OnSuccess();
}
}
std::string selected_text_;
DISALLOW_COPY_AND_ASSIGN(TextSelectionObserver);
};
// 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 {
public:
explicit RecordActiveViewsObserver(content::WebContents* web_contents)
: tester_(new content::TextInputManagerTester(web_contents)) {
tester_->SetUpdateTextInputStateCalledCallback(base::Bind(
&RecordActiveViewsObserver::RecordActiveView, base::Unretained(this)));
}
const std::vector<const content::RenderWidgetHostView*>* active_views()
const {
return &active_views_;
}
private:
void RecordActiveView() {
if (!tester_->IsTextInputStateChanged())
return;
active_views_.push_back(tester_->GetActiveView());
}
std::unique_ptr<content::TextInputManagerTester> tester_;
std::vector<const content::RenderWidgetHostView*> active_views_;
DISALLOW_COPY_AND_ASSIGN(RecordActiveViewsObserver);
};
} // namespace
// Main class for all TextInputState and IME related tests.
class SitePerProcessTextInputManagerTest : public InProcessBrowserTest {
public:
SitePerProcessTextInputManagerTest() {}
~SitePerProcessTextInputManagerTest() override {}
void SetUpCommandLine(base::CommandLine* command_line) override {
content::IsolateAllSitesForTesting(command_line);
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
// Add content/test/data 'cross_site_iframe_factory.html'.
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
}
protected:
content::WebContents* active_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
// static
// Adds an <input> field to a given frame by executing javascript code.
// The input can be added as the first element or the last element of
// |document.body|. The text range defined by |selection_range| will be
// marked.
static void AddInputFieldToFrame(content::RenderFrameHost* rfh,
const std::string& type,
const std::string& value,
bool append_as_first_child) {
std::string script = base::StringPrintf(
"var input = document.createElement('input');"
"input.setAttribute('type', '%s');"
"input.setAttribute('value', '%s');"
"document.body.%s;",
type.c_str(), value.c_str(),
append_as_first_child ? "insertBefore(input, document.body.firstChild)"
: "appendChild(input)");
EXPECT_TRUE(ExecuteScript(rfh, script));
}
// static
// Appends an <input> field with various attribues to a given frame by
// executing javascript code.
static void AppendInputFieldToFrame(content::RenderFrameHost* rfh,
const std::string& type,
const std::string& id,
const std::string& value,
const std::string& placeholder) {
std::string script = base::StringPrintf(
"var input = document.createElement('input');"
"input.setAttribute('type', '%s');"
"input.setAttribute('id', '%s');"
"input.setAttribute('value', '%s');"
"input.setAttribute('placeholder', '%s');"
"document.body.appendChild(input);",
type.c_str(), id.c_str(), value.c_str(), placeholder.c_str());
EXPECT_TRUE(ExecuteScript(rfh, script));
}
// static
// Focus a form field by its Id.
static void FocusFormField(content::RenderFrameHost* rfh,
const std::string& id) {
std::string script = base::StringPrintf(
"document.getElementById('%s').focus();", id.c_str());
EXPECT_TRUE(ExecuteScript(rfh, script));
}
// Uses 'cross_site_iframe_factory.html'. The main frame's domain is
// 'a.com'.
void CreateIframePage(const std::string& structure) {
std::string path = base::StringPrintf("/cross_site_iframe_factory.html?%s",
structure.c_str());
GURL main_url(embedded_test_server()->GetURL("a.com", path));
ui_test_utils::NavigateToURL(browser(), main_url);
}
// Iteratively uses ChildFrameAt(frame, i) to get the i-th child frame
// inside frame. For example, for 'a(b(c, d(e)))', [0] returns b, and
// [0, 1, 0] returns e;
content::RenderFrameHost* GetFrame(const IndexVector& indices) {
content::RenderFrameHost* current = active_contents()->GetMainFrame();
for (size_t index : indices)
current = ChildFrameAt(current, index);
return current;
}
private:
DISALLOW_COPY_AND_ASSIGN(SitePerProcessTextInputManagerTest);
};
// 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; https://crbug.com/704994.
#if defined(OS_CHROMEOS)
#define MAYBE_TrackStateWhenSwitchingFocusedFrames \
DISABLED_TrackStateWhenSwitchingFocusedFrames
#else
#define MAYBE_TrackStateWhenSwitchingFocusedFrames \
TrackStateWhenSwitchingFocusedFrames
#endif
IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
MAYBE_TrackStateWhenSwitchingFocusedFrames) {
CreateIframePage("a(a,b,c(a,b,d(e, f)),g)");
std::vector<std::string> values{
"main", "node_a", "node_b", "node_c", "node_c_a",
"node_c_b", "node_c_d", "node_c_d_e", "node_c_d_f", "node_g"};
// TODO(ekaramad): The use for explicitly constructing the IndexVector from
// initializer list should not be necessary. However, some chromeos bots throw
// errors if we do not do it like this.
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{2}),
GetFrame(IndexVector{2, 0}), GetFrame(IndexVector{2, 1}),
GetFrame(IndexVector{2, 2}), GetFrame(IndexVector{2, 2, 0}),
GetFrame(IndexVector{2, 2, 1}), GetFrame(IndexVector{3})};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", values[i], true);
for (size_t i = 0; i < frames.size(); ++i) {
TextInputManagerValueObserver observer(active_contents(), values[i]);
SimulateKeyPress(active_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
observer.Wait();
}
}
// 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) {
CreateIframePage("a(b, c)");
std::vector<std::string> values{"node_b", "node_c"};
std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{0}),
GetFrame(IndexVector{1})};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", values[i], true);
// Tab into both inputs and make sure we correctly receive their
// TextInputState. For the second tab two IPCs arrive: one from the first
// frame to set the state to none, and another one from the second frame to
// set it to TEXT. To avoid the race between them, we shall also observe the
// first frame setting its state to NONE after the second tab.
ViewTextInputTypeObserver view_type_observer(
active_contents(), frames[0]->GetView(), ui::TEXT_INPUT_TYPE_NONE);
for (size_t i = 0; i < frames.size(); ++i) {
TextInputManagerValueObserver observer(active_contents(), values[i]);
SimulateKeyPress(active_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
observer.Wait();
}
// Make sure that the first view has set its TextInputState.type to NONE.
view_type_observer.Wait();
// Verify that we are tracking the TextInputState from the first frame.
content::RenderWidgetHostView* first_view = frames[0]->GetView();
ui::TextInputType first_view_type;
EXPECT_TRUE(content::GetTextInputTypeForView(active_contents(), first_view,
&first_view_type));
EXPECT_EQ(ui::TEXT_INPUT_TYPE_NONE, first_view_type);
size_t registered_views_count =
content::GetRegisteredViewsCountFromTextInputManager(active_contents());
// We expect at least two views for the two child frames.
EXPECT_GT(registered_views_count, 2U);
// Now that the second frame's <input> is focused, we crash the first frame
// and observe that text input state is updated for the view.
std::unique_ptr<content::TestRenderWidgetHostViewDestructionObserver>
destruction_observer(
new content::TestRenderWidgetHostViewDestructionObserver(first_view));
frames[0]->GetProcess()->Shutdown(0);
destruction_observer->Wait();
// Verify that the TextInputManager is no longer tracking TextInputState for
// |first_view|.
EXPECT_EQ(
registered_views_count - 1U,
content::GetRegisteredViewsCountFromTextInputManager(active_contents()));
// Now crash the second <iframe> which has an active view.
destruction_observer.reset(
new content::TestRenderWidgetHostViewDestructionObserver(
frames[1]->GetView()));
frames[1]->GetProcess()->Shutdown(0);
destruction_observer->Wait();
EXPECT_EQ(
registered_views_count - 2U,
content::GetRegisteredViewsCountFromTextInputManager(active_contents()));
}
// 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) {
CreateIframePage("a(a, b)");
std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{0}),
GetFrame(IndexVector{1})};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", "", true);
// Press tab key to focus the <input> in the first frame.
TextInputManagerTypeObserver type_observer_text_a(active_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
SimulateKeyPress(active_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer_text_a.Wait();
std::string remove_first_iframe_script =
"var frame = document.querySelector('iframe');"
"frame.parentNode.removeChild(frame);";
// Detach first frame and observe |TextInputState.type| resetting to
// ui::TEXT_INPUT_TYPE_NONE.
TextInputManagerTypeObserver type_observer_none_a(active_contents(),
ui::TEXT_INPUT_TYPE_NONE);
EXPECT_TRUE(ExecuteScript(active_contents(), remove_first_iframe_script));
type_observer_none_a.Wait();
// Press tab to focus the <input> in the second frame.
TextInputManagerTypeObserver type_observer_text_b(active_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
SimulateKeyPress(active_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer_text_b.Wait();
// Detach first frame and observe |TextInputState.type| resetting to
// ui::TEXT_INPUT_TYPE_NONE.
TextInputManagerTypeObserver type_observer_none_b(active_contents(),
ui::TEXT_INPUT_TYPE_NONE);
EXPECT_TRUE(ExecuteScript(active_contents(), remove_first_iframe_script));
type_observer_none_b.Wait();
}
// 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) {
CreateIframePage("a(b)");
content::RenderFrameHost* main_frame = GetFrame(IndexVector{});
content::RenderFrameHost* child_frame = GetFrame(IndexVector{0});
AddInputFieldToFrame(child_frame, "text", "child", false);
// Focus <input> in child frame and verify the |TextInputState.value|.
TextInputManagerValueObserver child_set_state_observer(active_contents(),
"child");
SimulateKeyPress(active_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
child_set_state_observer.Wait();
// Navigate the child frame to about:blank and verify that TextInputManager
// correctly sets its |TextInputState.type| to ui::TEXT_INPUT_TYPE_NONE.
TextInputManagerTypeObserver child_reset_state_observer(
active_contents(), ui::TEXT_INPUT_TYPE_NONE);
EXPECT_TRUE(ExecuteScript(
main_frame, "document.querySelector('iframe').src = 'about:blank'"));
child_reset_state_observer.Wait();
}
// 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) {
CreateIframePage("a()");
content::RenderFrameHost* main_frame = GetFrame(IndexVector{});
AddInputFieldToFrame(main_frame, "text", "", false);
TextInputManagerTypeObserver set_state_observer(active_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
SimulateKeyPress(active_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
set_state_observer.Wait();
TextInputManagerTypeObserver reset_state_observer(active_contents(),
ui::TEXT_INPUT_TYPE_NONE);
ui_test_utils::NavigateToURL(browser(), GURL("about:blank"));
reset_state_observer.Wait();
}
#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) {
CreateIframePage("a()");
content::RenderFrameHost* main_frame = GetFrame(IndexVector{});
content::RenderWidgetHostView* view = main_frame->GetView();
content::WebContents* web_contents = active_contents();
AddInputFieldToFrame(main_frame, "text", "", false);
auto focus_input_and_wait_for_selection_bounds_change =
[&main_frame, &web_contents, &view]() {
ViewSelectionBoundsChangedObserver bounds_observer(web_contents, view);
// SimulateKeyPress(web_contents, ui::DomKey::TAB, ui::DomCode::TAB,
// ui::VKEY_TAB, false, true, false, false);
EXPECT_TRUE(ExecuteScript(main_frame,
"document.querySelector('input').focus();"));
bounds_observer.Wait();
};
focus_input_and_wait_for_selection_bounds_change();
// Focus location bar.
BrowserWindow* window = browser()->window();
ASSERT_TRUE(window);
LocationBar* location_bar = window->GetLocationBar();
ASSERT_TRUE(location_bar);
location_bar->FocusLocation();
focus_input_and_wait_for_selection_bounds_change();
}
#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) {
CreateIframePage("a()");
content::RenderFrameHost* main_frame = GetFrame(IndexVector{});
AddInputFieldToFrame(main_frame, "text", "", false);
// Focus the input and wait for state update.
TextInputManagerTypeObserver observer(active_contents(),
ui::TEXT_INPUT_TYPE_TEXT);
SimulateKeyPress(active_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
observer.Wait();
// Now destroy the tab. We should exit without crashing.
browser()->tab_strip_model()->CloseWebContentsAt(
0, TabStripModel::CLOSE_USER_GESTURE);
}
// 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) {
CreateIframePage("a(b,c(a,b),d)");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0}),
GetFrame(IndexVector{1, 1}), GetFrame(IndexVector{2})};
std::vector<content::RenderWidgetHostView*> views;
for (auto* frame : frames)
views.push_back(frame->GetView());
std::vector<std::string> values{"a", "ab", "ac", "aca", "acb", "acd"};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", values[i], true);
content::WebContents* web_contents = active_contents();
auto send_tab_and_wait_for_value =
[&web_contents](const std::string& expected_value) {
TextInputManagerValueObserver observer(web_contents, expected_value);
SimulateKeyPress(web_contents, ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
observer.Wait();
};
// Record all active view changes.
RecordActiveViewsObserver recorder(web_contents);
for (auto value : values)
send_tab_and_wait_for_value(value);
// We have covered a total of 6 views, so there should at least be 11 entries
// recorded (at least one null between two views).
size_t record_count = recorder.active_views()->size();
EXPECT_GT(record_count, 10U);
// Verify we do not have subsequent nullptr or non-nullptrs.
for (size_t i = 0; i < record_count - 1U; ++i) {
const content::RenderWidgetHostView* current =
recorder.active_views()->at(i);
const content::RenderWidgetHostView* next =
recorder.active_views()->at(i + 1U);
EXPECT_TRUE((current != nullptr && next == nullptr) ||
(current == nullptr && next != nullptr));
}
}
// 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) {
CreateIframePage("a(b,c(a,b),d)");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0}),
GetFrame(IndexVector{1, 1}), GetFrame(IndexVector{2})};
std::vector<content::RenderWidgetHostView*> views;
for (auto* frame : frames)
views.push_back(frame->GetView());
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", "text", true);
content::WebContents* web_contents = active_contents();
auto send_tab_set_composition_wait_for_bounds_change = [&web_contents](
content::RenderWidgetHostView* view) {
ViewTextInputTypeObserver type_observer(web_contents, view,
ui::TEXT_INPUT_TYPE_TEXT);
SimulateKeyPress(web_contents, ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer.Wait();
ViewCompositionRangeChangedObserver range_observer(web_contents, view);
EXPECT_TRUE(content::RequestCompositionInfoFromActiveWidget(web_contents));
range_observer.Wait();
};
for (auto* view : views)
send_tab_set_composition_wait_for_bounds_change(view);
}
// Failing on Mac - http://crbug.com/852452
#if defined(OS_MACOSX)
#define MAYBE_TrackTextSelectionForAllFrames \
DISABLED_TrackTextSelectionForAllFrames
#else
#define MAYBE_TrackTextSelectionForAllFrames 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) {
CreateIframePage("a(b,c(a,b),d)");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0}),
GetFrame(IndexVector{1, 1}), GetFrame(IndexVector{2})};
std::vector<std::string> values{"main", "b", "c", "ca", "cb", "d"};
std::vector<content::RenderWidgetHostView*> views;
for (auto* frame : frames)
views.push_back(frame->GetView());
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", values[i], true);
content::WebContents* web_contents = active_contents();
auto send_tab_and_wait_for_value = [&web_contents](const std::string& value) {
TextInputManagerValueObserver observer(web_contents, value);
SimulateKeyPress(web_contents, ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
observer.Wait();
};
auto send_keys_select_all_wait_for_selection_change = [&web_contents](
content::RenderWidgetHostView* view, size_t count) {
ViewTextSelectionObserver observer(web_contents, view, count);
for (size_t i = 0; i < count; ++i) {
SimulateKeyPress(web_contents, ui::DomKey::FromCharacter('E'),
ui::DomCode::US_E, ui::VKEY_E, false, false, false,
false);
}
observer.Wait();
};
size_t count = 2;
for (size_t i = 0; i < views.size(); ++i) {
// First focus the <input>.
send_tab_and_wait_for_value(values[i]);
// Send a sequence of |count| 'E' keys and wait until the view receives a
// selection change update for a text of the corresponding size, |count|.
send_keys_select_all_wait_for_selection_change(views[i], count++);
}
}
// 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 defined(OS_MACOSX)
#define MAYBE_ImeCommitTextForAllFrames DISABLED_ImeCommitTextForAllFrames
#else
#define MAYBE_ImeCommitTextForAllFrames ImeCommitTextForAllFrames
#endif
IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
MAYBE_ImeCommitTextForAllFrames) {
CreateIframePage("a(b,c(a))");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0})};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", "", true);
std::vector<std::string> sample_text{"main", "child_b", "child_c", "child_a"};
ASSERT_EQ(frames.size(), sample_text.size());
// An observer of all text selection updates within a WebContents.
TextSelectionObserver observer(active_contents());
for (size_t index = 0; index < frames.size(); ++index) {
// Focus the input and listen to 'input' event inside the frame. When the
// event fires, select all the text inside the input. This will trigger a
// selection update on the browser side.
ASSERT_TRUE(ExecuteScript(frames[index],
"window.focus();"
"var input = document.querySelector('input');"
"input.focus();"
"window.addEventListener('input', function(e) {"
" input.select();"
"});"))
<< "Could not run script in frame with index:" << index;
// Commit some text for this frame.
content::SendImeCommitTextToWidget(
frames[index]->GetView()->GetRenderWidgetHost(),
base::UTF8ToUTF16(sample_text[index]), std::vector<ui::ImeTextSpan>(),
gfx::Range(), 0);
// Verify that the text we committed is now selected by listening to a
// selection update from a RenderWidgetHostView which has the expected
// selected text.
observer.WaitForSelectedText(sample_text[index]);
}
}
// 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 !defined(OS_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) {
CreateIframePage("a(b,c(a,b),d)");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0}),
GetFrame(IndexVector{1, 1}), GetFrame(IndexVector{2})};
std::vector<content::RenderWidgetHostView*> views;
for (auto* frame : frames)
views.push_back(frame->GetView());
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", "", true);
content::WebContents* web_contents = active_contents();
auto send_tab_insert_text_wait_for_bounds_change = [&web_contents](
content::RenderWidgetHostView* view) {
ViewTextInputTypeObserver type_observer(web_contents, view,
ui::TEXT_INPUT_TYPE_TEXT);
SimulateKeyPress(web_contents, ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
type_observer.Wait();
ViewSelectionBoundsChangedObserver bounds_observer(web_contents, view);
SimulateKeyPress(web_contents, ui::DomKey::FromCharacter('E'),
ui::DomCode::US_E, ui::VKEY_E, false, false, false, false);
bounds_observer.Wait();
};
for (auto* view : views)
send_tab_insert_text_wait_for_bounds_change(view);
}
// This test makes sure browser correctly tracks focused editable element inside
// each RenderFrameHost.
// Test is flaky on chromeOS; https://crbug.com/705203.
#if defined(OS_CHROMEOS)
#define MAYBE_TrackingFocusedElementForAllFrames \
DISABLED_TrackingFocusedElementForAllFrames
#else
#define MAYBE_TrackingFocusedElementForAllFrames \
TrackingFocusedElementForAllFrames
#endif
IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
MAYBE_TrackingFocusedElementForAllFrames) {
CreateIframePage("a(a, b(a))");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0})};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", "some text", true);
// Focus the <input> in |frame| and return if RenderFrameHost thinks there is
// a focused editable element in it.
auto focus_input_and_return_editable_element_state =
[](content::RenderFrameHost* frame) {
EXPECT_TRUE(
ExecuteScript(frame, "document.querySelector('input').focus();"));
return content::DoesFrameHaveFocusedEditableElement(frame);
};
// When focusing an <input> we should receive an update.
for (auto* frame : frames)
EXPECT_TRUE(focus_input_and_return_editable_element_state(frame));
// Blur the <input> in |frame| and return if RenderFrameHost thinks there is a
// focused editable element in it.
auto blur_input_and_return_editable_element_state =
[](content::RenderFrameHost* frame) {
EXPECT_TRUE(
ExecuteScript(frame, "document.querySelector('input').blur();"));
return content::DoesFrameHaveFocusedEditableElement(frame);
};
// Similarly, we should receive updates when losing focus.
for (auto* frame : frames)
EXPECT_FALSE(blur_input_and_return_editable_element_state(frame));
}
// 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 defined(OS_CHROMEOS)
#define MAYBE_TrackPageFocusEditableElement \
DISABLED_TrackPageFocusEditableElement
#else
#define MAYBE_TrackPageFocusEditableElement TrackPageFocusEditableElement
#endif
IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
MAYBE_TrackPageFocusEditableElement) {
CreateIframePage("a(a, b(a))");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0})};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", "some text", true);
auto focus_frame = [](content::RenderFrameHost* frame) {
EXPECT_TRUE(ExecuteScript(frame, "window.focus();"));
};
auto set_input_focus = [](content::RenderFrameHost* frame, bool focus) {
EXPECT_TRUE(ExecuteScript(
frame, base::StringPrintf("document.querySelector('input').%s();",
(focus ? "focus" : "blur"))));
};
for (auto* frame : frames) {
focus_frame(frame);
// Focus the <input>.
set_input_focus(frame, true);
EXPECT_TRUE(active_contents()->IsFocusedElementEditable());
// No blur <input>.
set_input_focus(frame, false);
EXPECT_FALSE(active_contents()->IsFocusedElementEditable());
}
}
// 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 defined(OS_CHROMEOS)
#define MAYBE_ClearFocusedElementOnPage DISABLED_ClearFocusedElementOnPage
#else
#define MAYBE_ClearFocusedElementOnPage ClearFocusedElementOnPage
#endif
IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
MAYBE_ClearFocusedElementOnPage) {
CreateIframePage("a(a, b(a))");
std::vector<content::RenderFrameHost*> frames{
GetFrame(IndexVector{}), GetFrame(IndexVector{0}),
GetFrame(IndexVector{1}), GetFrame(IndexVector{1, 0})};
for (size_t i = 0; i < frames.size(); ++i)
AddInputFieldToFrame(frames[i], "text", "some text", true);
auto focus_frame_and_input = [](content::RenderFrameHost* frame) {
EXPECT_TRUE(ExecuteScript(frame,
"window.focus();"
"document.querySelector('input').focus();"));
};
for (auto* frame : frames) {
focus_frame_and_input(frame);
EXPECT_TRUE(active_contents()->IsFocusedElementEditable());
active_contents()->ClearFocusedElement();
EXPECT_FALSE(active_contents()->IsFocusedElementEditable());
}
}
// 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 {
public:
explicit InputMethodObserverBase(content::WebContents* web_contents)
: success_(false),
test_observer_(content::TestInputMethodObserver::Create(web_contents)) {
}
void Wait() {
if (success_)
return;
message_loop_runner_ = new content::MessageLoopRunner();
message_loop_runner_->Run();
}
bool success() const { return success_; }
protected:
content::TestInputMethodObserver* test_observer() {
return test_observer_.get();
}
const base::Closure success_closure() {
return base::Bind(&InputMethodObserverBase::OnSuccess,
base::Unretained(this));
}
private:
void OnSuccess() {
success_ = true;
if (message_loop_runner_)
message_loop_runner_->Quit();
}
bool success_;
std::unique_ptr<content::TestInputMethodObserver> test_observer_;
scoped_refptr<content::MessageLoopRunner> message_loop_runner_;
DISALLOW_COPY_AND_ASSIGN(InputMethodObserverBase);
};
class InputMethodObserverForShowIme : public InputMethodObserverBase {
public:
explicit InputMethodObserverForShowIme(content::WebContents* web_contents)
: InputMethodObserverBase(web_contents) {
test_observer()->SetOnShowVirtualKeyboardIfEnabledCallback(
success_closure());
}
private:
DISALLOW_COPY_AND_ASSIGN(InputMethodObserverForShowIme);
};
// 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|.
IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
CorrectlyShowVirtualKeyboardIfEnabled) {
// We only need the <iframe> page to create RWHV.
CreateIframePage("a()");
content::RenderFrameHost* main_frame = GetFrame(IndexVector{});
content::RenderWidgetHostView* view = main_frame->GetView();
content::WebContents* web_contents = active_contents();
content::TextInputStateSender sender(view);
auto send_and_check_show_ime = [&sender, &web_contents]() {
InputMethodObserverForShowIme observer(web_contents);
sender.Send();
return observer.success();
};
// Sending an empty state should not trigger ime.
EXPECT_FALSE(send_and_check_show_ime());
// Set |TextInputState.type| to text. Expect no IME.
sender.SetType(ui::TEXT_INPUT_TYPE_TEXT);
EXPECT_FALSE(send_and_check_show_ime());
// Set |TextInputState.show_ime_if_needed| to true. Expect IME.
sender.SetShowVirtualKeyboardIfEnabled(true);
#if defined(OS_WIN)
sender.SetLastPointerType(ui::EventPointerType::POINTER_TYPE_TOUCH);
#endif
EXPECT_TRUE(send_and_check_show_ime());
// Send the same message. Expect IME (no change).
EXPECT_TRUE(send_and_check_show_ime());
// Reset |TextInputState.show_ime_if_needed|. Expect no IME.
sender.SetShowVirtualKeyboardIfEnabled(false);
EXPECT_FALSE(send_and_check_show_ime());
// Setting an irrelevant field. Expect no IME.
sender.SetMode(ui::TEXT_INPUT_MODE_TEXT);
EXPECT_FALSE(send_and_check_show_ime());
// Set |TextInputState.show_ime_if_needed|. Expect IME.
sender.SetShowVirtualKeyboardIfEnabled(true);
EXPECT_TRUE(send_and_check_show_ime());
#if defined(OS_WIN)
// Set input type to mouse. Expect no IME.
sender.SetLastPointerType(ui::EventPointerType::POINTER_TYPE_MOUSE);
EXPECT_FALSE(send_and_check_show_ime());
#endif
// Set |TextInputState.type| to ui::TEXT_INPUT_TYPE_NONE. Expect no IME.
sender.SetType(ui::TEXT_INPUT_TYPE_NONE);
EXPECT_FALSE(send_and_check_show_ime());
}
#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 TextEditKeyBindingsDelegateAuraLinux, which only exists on Linux.
#if defined(OS_LINUX) && !defined(OS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest,
SubframeKeyboardEditCommands) {
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/frame_tree/page_with_one_frame.html"));
ui_test_utils::NavigateToURL(browser(), main_url);
content::WebContents* web_contents = active_contents();
GURL frame_url(
embedded_test_server()->GetURL("b.com", "/page_with_input_field.html"));
EXPECT_TRUE(NavigateIframeToURL(web_contents, "child0", frame_url));
// Focus the subframe and then its input field. The return value
// "input-focus" will be sent once the input field's focus event fires.
content::RenderFrameHost* child =
ChildFrameAt(web_contents->GetMainFrame(), 0);
std::string result;
std::string script =
"function onInput(e) {"
" domAutomationController.send(getInputFieldText());"
"}"
"inputField = document.getElementById('text-field');"
"inputField.addEventListener('input', onInput, false);";
EXPECT_TRUE(ExecuteScript(child, script));
EXPECT_TRUE(ExecuteScriptAndExtractString(
child, "window.focus(); focusInputField();", &result));
EXPECT_EQ("input-focus", result);
EXPECT_EQ(child, web_contents->GetFocusedFrame());
// Generate a couple of keystrokes, which will be routed to the subframe.
content::DOMMessageQueue msg_queue;
std::string reply;
SimulateKeyPress(web_contents, ui::DomKey::FromCharacter('1'),
ui::DomCode::DIGIT1, ui::VKEY_1, false, false, false, false);
EXPECT_TRUE(msg_queue.WaitForMessage(&reply));
SimulateKeyPress(web_contents, ui::DomKey::FromCharacter('2'),
ui::DomCode::DIGIT2, ui::VKEY_2, false, false, false, false);
EXPECT_TRUE(msg_queue.WaitForMessage(&reply));
// Verify that the input field in the subframe received the keystrokes.
EXPECT_TRUE(ExecuteScriptAndExtractString(
child, "window.domAutomationController.send(getInputFieldText());",
&result));
EXPECT_EQ("12", result);
// Define and install a test delegate that translates any keystroke to a
// command to delete all text from current cursor position to the beginning
// of the line.
class TextDeleteDelegate : public ui::TextEditKeyBindingsDelegateAuraLinux {
public:
TextDeleteDelegate() {}
~TextDeleteDelegate() override {}
bool MatchEvent(
const ui::Event& event,
std::vector<ui::TextEditCommandAuraLinux>* commands) override {
if (commands) {
commands->emplace_back(ui::TextEditCommand::DELETE_TO_BEGINNING_OF_LINE,
"");
}
return true;
}
private:
DISALLOW_COPY_AND_ASSIGN(TextDeleteDelegate);
};
TextDeleteDelegate delegate;
ui::TextEditKeyBindingsDelegateAuraLinux* old_delegate =
ui::GetTextEditKeyBindingsDelegate();
ui::SetTextEditKeyBindingsDelegate(&delegate);
// Press ctrl-alt-shift-D. The test's delegate will pretend that this
// corresponds to the command to delete everyting to the beginning of the
// line. Note the use of SendKeyPressSync instead of SimulateKeyPress, as
// the latter doesn't go through
// RenderWidgetHostViewAura::ForwardKeyboardEvent, which contains the edit
// commands logic that's tested here.
ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_D,
true, true, true, false));
ui::SetTextEditKeyBindingsDelegate(old_delegate);
// Verify that the input field in the subframe is erased.
EXPECT_TRUE(ExecuteScriptAndExtractString(
child, "window.domAutomationController.send(getInputFieldText());",
&result));
EXPECT_EQ("", result);
}
#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 defined(OS_MACOSX)
class ShowDefinitionForWordObserver
: content::RenderWidgetHostViewCocoaObserver {
public:
explicit ShowDefinitionForWordObserver(content::WebContents* web_contents)
: content::RenderWidgetHostViewCocoaObserver(web_contents) {}
~ShowDefinitionForWordObserver() override {}
const std::string& WaitForWordLookUp() {
if (did_receive_string_)
return word_;
run_loop_.reset(new 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_;
DISALLOW_COPY_AND_ASSIGN(ShowDefinitionForWordObserver);
};
// 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(
ExecuteScript(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());
}
}
// The original TextInputClientMessageFilter is added during the initialization
// phase of RenderProcessHost. The only chance we have to add the test filter
// (so that it can receive the TextInputClientMac incoming IPC messages) is
// during the call to RenderProcessWillLaunch() on ContentBrowserClient. This
// class provides that for testing.
class TestBrowserClient : public ChromeContentBrowserClient {
public:
TestBrowserClient() {
old_client_ = content::SetBrowserClientForTesting(this);
}
~TestBrowserClient() override {
content::SetBrowserClientForTesting(old_client_);
}
// ContentBrowserClient overrides.
void RenderProcessWillLaunch(
content::RenderProcessHost* process_host,
service_manager::mojom::ServiceRequest* service_request) override {
ChromeContentBrowserClient::RenderProcessWillLaunch(process_host,
service_request);
filters_.push_back(
new content::TestTextInputClientMessageFilter(process_host));
}
// Retrieves the registered filter for the given RenderProcessHost. It will
// return false if the RenderProcessHost was initialized while a different
// instance of ContentBrowserClient was in action.
scoped_refptr<content::TestTextInputClientMessageFilter>
GetTextInputClientMessageFilterForProcess(
content::RenderProcessHost* process_host) const {
for (auto filter : filters_) {
if (filter->process() == process_host)
return filter;
}
return nullptr;
}
private:
content::ContentBrowserClient* old_client_ = nullptr;
std::vector<scoped_refptr<content::TestTextInputClientMessageFilter>>
filters_;
DISALLOW_COPY_AND_ASSIGN(TestBrowserClient);
};
// Earlier injection of TestBrowserClient (a ContentBrowserClient) is needed to
// make sure it is active during creation of the first spare RenderProcessHost.
// Without this change, the tests would be surprised that they cannot find an
// injected message filter via GetTextInputClientMessageFilterForProcess.
class SitePerProcessCustomTextInputManagerFilteringTest
: public SitePerProcessTextInputManagerTest {
public:
SitePerProcessCustomTextInputManagerFilteringTest() {}
~SitePerProcessCustomTextInputManagerFilteringTest() override {}
void CreatedBrowserMainParts(content::BrowserMainParts* parts) override {
SitePerProcessTextInputManagerTest::CreatedBrowserMainParts(parts);
browser_client_ = std::make_unique<TestBrowserClient>();
}
void TearDown() override {
browser_client_.reset();
SitePerProcessTextInputManagerTest::TearDown();
}
scoped_refptr<content::TestTextInputClientMessageFilter>
GetTextInputClientMessageFilterForProcess(
content::RenderProcessHost* process_host) const {
return browser_client_->GetTextInputClientMessageFilterForProcess(
process_host);
}
private:
std::unique_ptr<TestBrowserClient> browser_client_;
DISALLOW_COPY_AND_ASSIGN(SitePerProcessCustomTextInputManagerFilteringTest);
};
// 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(
SitePerProcessCustomTextInputManagerFilteringTest,
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),
TabStripModel::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(ExecuteScript(child_frame,
"window.focus();"
"document.querySelector('input').focus();"
"document.querySelector('input').select();"));
content::RenderWidgetHostView* child_view = child_frame->GetView();
scoped_refptr<content::TestTextInputClientMessageFilter>
child_message_filter = GetTextInputClientMessageFilterForProcess(
child_view->GetRenderWidgetHost()->GetProcess());
DCHECK(child_message_filter);
// 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 IPC is received. The destruction is post tasked to UI thread.
int32_t child_process_id =
child_view->GetRenderWidgetHost()->GetProcess()->GetID();
int32_t child_frame_routing_id = child_frame->GetRoutingID();
child_message_filter->SetStringForRangeCallback(base::Bind(
[](int32_t process_id, int32_t routing_id,
const base::Closure& callback_on_io) {
// This runs before TextInputClientMac gets to handle the IPC. 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.
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::IO},
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(
SitePerProcessCustomTextInputManagerFilteringTest,
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),
TabStripModel::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(ExecuteScript(main_frame,
"document.querySelector('input').focus();"
"document.querySelector('input').select();"));
content::RenderWidgetHostView* page_rwhv = main_frame->GetView();
scoped_refptr<content::TestTextInputClientMessageFilter> message_filter =
GetTextInputClientMessageFilterForProcess(
page_rwhv->GetRenderWidgetHost()->GetProcess());
DCHECK(message_filter);
// 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 IPC is received. The destruction is post tasked to UI thread.
int32_t main_frame_process_id =
page_rwhv->GetRenderWidgetHost()->GetProcess()->GetID();
int32_t main_frame_routing_id = main_frame->GetRoutingID();
message_filter->SetStringForRangeCallback(base::Bind(
[](int32_t process_id, int32_t routing_id,
const base::Closure& callback_on_io) {
// This runs before TextInputClientMac gets to handle the IPC. 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.
base::PostTaskWithTraits(FROM_HERE, {content::BrowserThread::IO},
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 // !defined(OS_ANDROID)