Index: chrome/browser/renderer_host/site_per_process_text_input_browsertest.cc |
diff --git a/chrome/browser/renderer_host/site_per_process_text_input_browsertest.cc b/chrome/browser/renderer_host/site_per_process_text_input_browsertest.cc |
index 736216abba0d6fb28e31ddc52297f86324279624..5ad23c74640c2fdc55f325b0614cf515d6ded320 100644 |
--- a/chrome/browser/renderer_host/site_per_process_text_input_browsertest.cc |
+++ b/chrome/browser/renderer_host/site_per_process_text_input_browsertest.cc |
@@ -26,6 +26,7 @@ |
#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/clipboard/clipboard.h" |
#include "ui/base/ime/composition_underline.h" |
#include "ui/base/ime/text_edit_commands.h" |
#include "ui/base/ime/text_input_client.h" |
@@ -144,22 +145,31 @@ class TextInputManagerTypeObserver : public TextInputManagerObserverBase { |
DISALLOW_COPY_AND_ASSIGN(TextInputManagerTypeObserver); |
}; |
-// This class observes TextInputManager for the first change in TextInputState. |
-class TextInputManagerChangeObserver : public TextInputManagerObserverBase { |
+// This class observes TextInputManager for changes in |
+// |TextInputState.selection_start|. |
+class TextInputManagerCursorPositionObserver |
+ : public TextInputManagerObserverBase { |
public: |
- explicit TextInputManagerChangeObserver(content::WebContents* web_contents) |
- : TextInputManagerObserverBase(web_contents) { |
- tester()->SetUpdateTextInputStateCalledCallback(base::Bind( |
- &TextInputManagerChangeObserver::VerifyChange, base::Unretained(this))); |
+ TextInputManagerCursorPositionObserver(content::WebContents* web_contents, |
+ int expected_position) |
+ : TextInputManagerObserverBase(web_contents), |
+ expected_position_(expected_position) { |
+ tester()->SetUpdateTextInputStateCalledCallback( |
+ base::Bind(&TextInputManagerCursorPositionObserver::VerifyPosition, |
+ base::Unretained(this))); |
} |
private: |
- void VerifyChange() { |
- if (tester()->IsTextInputStateChanged()) |
+ void VerifyPosition() { |
+ int position = |
+ tester()->GetTextInputCursorPosition(&position) ? position : -1; |
+ if (expected_position_ == position) |
OnSuccess(); |
} |
- DISALLOW_COPY_AND_ASSIGN(TextInputManagerChangeObserver); |
+ const int expected_position_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(TextInputManagerCursorPositionObserver); |
}; |
// This class observes |TextInputState.type| for a specific RWHV. |
@@ -276,25 +286,55 @@ class ViewTextSelectionObserver : public TextInputManagerObserverBase { |
class TextSelectionObserver : public TextInputManagerObserverBase { |
public: |
explicit TextSelectionObserver(content::WebContents* web_contents) |
- : TextInputManagerObserverBase(web_contents) { |
+ : TextInputManagerObserverBase(web_contents), |
+ selection_changed_count_(0) { |
tester()->SetOnTextSelectionChangedCallback(base::Bind( |
&TextSelectionObserver::VerifyChange, base::Unretained(this))); |
} |
- void WaitForSelectedText(const std::string& text) { |
- selected_text_ = text; |
- Wait(); |
+ void WaitForSelectedText(const std::string& text, |
+ bool user_initiated = true) { |
+ expected_text_ = text; |
+ expected_user_initiated_ = user_initiated; |
+ if (last_selected_text_.has_value() && |
+ last_selected_text_ == expected_text_ && |
+ last_user_initiated_ == expected_user_initiated_) { |
+ OnSuccess(); |
+ } else { |
+ Wait(); |
+ } |
} |
+ int selection_changed_count() { return selection_changed_count_; } |
+ |
private: |
void VerifyChange() { |
- if (base::UTF16ToUTF8(tester()->GetUpdatedView()->GetSelectedText()) == |
- selected_text_) { |
+ selection_changed_count_++; |
+ |
+ last_selected_text_ = |
+ base::UTF16ToUTF8(tester()->GetUpdatedView()->GetSelectedText()); |
+ EXPECT_TRUE(tester()->GetTextSelectionUserInitiatedForView( |
+ tester()->GetUpdatedView(), &last_user_initiated_)); |
+ |
+ // Check whether the expected values are already set. |
+ if (!expected_text_.has_value()) |
+ return; |
+ |
+ if (last_selected_text_ == expected_text_ && |
+ last_user_initiated_ == expected_user_initiated_) { |
OnSuccess(); |
} |
} |
- std::string selected_text_; |
+ // These optional properties are also used to verify that the last and |
+ // expected properties are set. |
+ base::Optional<std::string> last_selected_text_; |
+ base::Optional<std::string> expected_text_; |
+ |
+ bool last_user_initiated_; |
+ bool expected_user_initiated_; |
+ |
+ int selection_changed_count_; |
DISALLOW_COPY_AND_ASSIGN(TextSelectionObserver); |
}; |
@@ -982,6 +1022,280 @@ IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, |
} |
} |
+// The following tests verify that the TextInputManager notifies about a |
+// text selection change event, the corresponding |user_initiated| property |
+// of TextSelection is valid, and the selection clipboard is updated |
+// according to the source of the event. |
+// See: https://crbug.com/671986 |
+ |
+// Test text selection change event for non-user initiated cases |
+// (eg. JavaScript). Non-user initiated events should not update the selection |
+// clipboard. |
+IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, |
+ NonUserInitiatedTextSelection) { |
+#if defined(USE_AURA) && defined(OS_LINUX) && !defined(OS_CHROMEOS) |
+ ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); |
+ ASSERT_TRUE(clipboard); |
+ clipboard->Clear(ui::CLIPBOARD_TYPE_SELECTION); |
+#endif |
+ |
+ CreateIframePage("a(b, c)"); |
+ std::vector<std::string> values{"node_a", "node_b", "node_c"}; |
+ std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{}), |
+ GetFrame(IndexVector{0}), |
+ GetFrame(IndexVector{1})}; |
+ |
+ for (size_t i = 0; i < frames.size(); ++i) |
+ AddInputFieldToFrame(frames[i], "text", values[i], true); |
+ |
+ // Test text selection from JavaScript across frames (non-user initiated). |
+ for (size_t i = 0; i < frames.size(); ++i) { |
+ // Trigger text selection. |
+ TextSelectionObserver trigger_observer(active_contents()); |
+ EXPECT_TRUE( |
+ ExecuteScript(frames[i], "document.querySelector('input').select();")); |
+ trigger_observer.WaitForSelectedText(values[i], false); |
+ |
+#if defined(USE_AURA) && defined(OS_LINUX) && !defined(OS_CHROMEOS) |
+ // Non-user initiated text selection should not update the selection |
+ // clipboard. See: https://crbug.com/12392 |
+ base::string16 result_text; |
+ clipboard->ReadText(ui::CLIPBOARD_TYPE_SELECTION, &result_text); |
+ EXPECT_TRUE(result_text.empty()); |
+#endif |
+ |
+ // Clear text selection. |
+ TextSelectionObserver clear_observer(active_contents()); |
+ EXPECT_TRUE(ExecuteScript(frames[i], "document.getSelection().empty();")); |
+ clear_observer.WaitForSelectedText("", false); |
+ } |
+} |
+ |
+// Test text selection change event for user initiated cases (eg. key press) |
+// User initiated events should update the selection clipboard where it is |
+// supported. |
+IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, |
+ UserInitiatedTextSelection) { |
+#if defined(USE_AURA) && defined(OS_LINUX) && !defined(OS_CHROMEOS) |
+ ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); |
+ ASSERT_TRUE(clipboard); |
+ clipboard->Clear(ui::CLIPBOARD_TYPE_SELECTION); |
+#endif |
+ |
+ CreateIframePage("a(b, c)"); |
+ std::vector<std::string> values{"node_a", "node_b", "node_c"}; |
+ std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{}), |
+ GetFrame(IndexVector{0}), |
+ GetFrame(IndexVector{1})}; |
+ |
+ for (size_t i = 0; i < frames.size(); ++i) |
+ AddInputFieldToFrame(frames[i], "text", values[i], true); |
+ |
+ // Test text selection by user input across frames (user initiated). |
+ for (size_t i = 0; i < frames.size(); ++i) { |
+ // Focus on input element. |
+ std::string result; |
+ std::string script = |
+ "function getInputField() {" |
+ " return document.querySelector('input');" |
+ "}" |
+ "function onInputFocus(e) {" |
+ " domAutomationController.setAutomationId(0);" |
+ " domAutomationController.send(getInputField().value);" |
+ "}" |
+ "getInputField().addEventListener('focus', onInputFocus);"; |
+ EXPECT_TRUE(ExecuteScript(frames[i], script)); |
+ EXPECT_TRUE(ExecuteScriptAndExtractString( |
+ frames[i], "window.focus(); document.querySelector('input').focus();", |
+ &result)); |
+ EXPECT_EQ(values[i], result); |
+ EXPECT_EQ(frames[i], active_contents()->GetFocusedFrame()); |
+ |
+ // Press ctrl+a to select text in the input field. |
+ TextSelectionObserver trigger_observer(active_contents()); |
+#if !defined(OS_MACOSX) |
+ SimulateKeyPress(active_contents(), ui::DomKey::FromCharacter('A'), |
+ ui::DomCode::US_A, ui::VKEY_A, true, false, false, false); |
+#else |
+ // On macOS the select all shortcut (Cmd+A) is handled by the browser via |
+ // keyboard accelerator. Thus we can't simulate key press on WebContents. |
+ // As a workaround, call the SelectAll directly as the shortcut would do. |
+ active_contents()->SelectAll(); |
+#endif |
+ trigger_observer.WaitForSelectedText(values[i], true); |
+ |
+#if defined(USE_AURA) && defined(OS_LINUX) && !defined(OS_CHROMEOS) |
+ // User initiated text selection should update the selection clipboard. |
+ base::string16 result_text; |
+ clipboard->ReadText(ui::CLIPBOARD_TYPE_SELECTION, &result_text); |
+ EXPECT_EQ(base::ASCIIToUTF16(values[i]), result_text); |
+#endif |
+ |
+ // Press down key to clear text selection. |
+ TextSelectionObserver clear_observer(active_contents()); |
+ SimulateKeyPress(active_contents(), ui::DomKey::ARROW_DOWN, |
+ ui::DomCode::ARROW_DOWN, ui::VKEY_DOWN, false, false, |
+ false, false); |
+ clear_observer.WaitForSelectedText("", true); |
+ } |
+} |
+ |
+// This test changes focus between input fields and checks that no selection |
+// changed event was triggered when Blink tries to clear a potential text |
+// selection on the previous input field where there was no text selection. |
+IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, |
+ TextSelectionOnFocusChange) { |
+ CreateIframePage("a(b, c)"); |
+ std::vector<std::string> values{"node_a", "node_b", "node_c"}; |
+ std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{}), |
+ GetFrame(IndexVector{0}), |
+ GetFrame(IndexVector{1})}; |
+ |
+ for (size_t i = 0; i < frames.size(); ++i) { |
+ AddInputFieldToFrame(frames[i], "text", values[i], true); |
+ AddInputFieldToFrame(frames[i], "number", std::to_string(i), true); |
+ } |
+ |
+ for (size_t i = 0; i < frames.size(); ++i) { |
+ std::string script = |
+ "function onInputFocus(e) {" |
+ " domAutomationController.setAutomationId(0);" |
+ " domAutomationController.send(document.activeElement.value);" |
+ "}" |
+ "var text_input = document.querySelector('input[type=text]');" |
+ "text_input.addEventListener('focus', onInputFocus);" |
+ "var number_input = document.querySelector('input[type=number]');" |
+ "number_input.addEventListener('focus', onInputFocus);"; |
+ EXPECT_TRUE(ExecuteScript(frames[i], script)); |
+ TextSelectionObserver selection_observer(active_contents()); |
+ |
+ // Focus on text input element. |
+ std::string text_result; |
+ EXPECT_TRUE(ExecuteScriptAndExtractString( |
+ frames[i], |
+ "window.focus(); document.querySelector('input[type=text]').focus();", |
+ &text_result)); |
+ EXPECT_EQ(values[i], text_result); |
+ EXPECT_EQ(0, selection_observer.selection_changed_count()); |
+ |
+ // Focus on number input element. |
+ std::string number_result; |
+ EXPECT_TRUE(ExecuteScriptAndExtractString( |
+ frames[i], |
+ "window.focus(); document.querySelector('input[type=number]').focus();", |
+ &number_result)); |
+ EXPECT_EQ(std::to_string(i), number_result); |
+ EXPECT_EQ(0, selection_observer.selection_changed_count()); |
+ } |
+} |
+ |
+// This test replaces text in input filed in IME composition mode. The text is |
+// replaced by using text selection during the composition mode. Verify that |
+// the text replacement doesn't trigger any selection changed event. |
+IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, |
+ ImeSetCompositionTextReplacement) { |
+ CreateIframePage("a(b, c)"); |
+ std::vector<std::string> values{"node_a", "node_b", "node_c"}; |
+ std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{}), |
+ GetFrame(IndexVector{0}), |
+ GetFrame(IndexVector{1})}; |
+ |
+ 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) { |
+ // Focus on input element. |
+ EXPECT_TRUE(ExecuteScript(frames[i], |
+ "window.focus();" |
+ "document.querySelector('input').focus();")); |
+ |
+ TextSelectionObserver selection_observer(active_contents()); |
+ |
+ // Do text replacement in composition mode. |
+ ViewCompositionRangeChangedObserver range_observer_set_composition( |
+ active_contents(), frames[i]->GetView()); |
+ content::SendImeSetCompositionTextToWidget( |
+ frames[i]->GetView()->GetRenderWidgetHost(), |
+ base::UTF8ToUTF16(values[i]), std::vector<ui::CompositionUnderline>(), |
+ gfx::Range(5, 1), 0, 0); |
+ range_observer_set_composition.Wait(); |
+ |
+ // Check composition text. |
+ std::string result; |
+ EXPECT_TRUE(ExecuteScriptAndExtractString( |
+ frames[i], |
+ "domAutomationController.setAutomationId(0);" |
+ "var value = document.querySelector('input').value;" |
+ "domAutomationController.send(value);", |
+ &result)); |
+ EXPECT_EQ("node_" + values[i], result); |
+ |
+ // Text selection should not be triggered by IME composition text |
+ // replacement. |
+ EXPECT_EQ(0, selection_observer.selection_changed_count()); |
+ } |
+} |
+ |
+// This test changes the cursor position in an input field from JavaScript. |
+// Verify that the cursor position |TextInputState.selection_start| always |
+// updated but non-user initiated text selection change is only triggered if |
+// an existing selection is changed. |
+IN_PROC_BROWSER_TEST_F(SitePerProcessTextInputManagerTest, JSCursorMovement) { |
+ CreateIframePage("a(b, c)"); |
+ std::vector<std::string> values{"node_a", "node_b", "node_c"}; |
+ std::vector<content::RenderFrameHost*> frames{GetFrame(IndexVector{}), |
+ GetFrame(IndexVector{0}), |
+ GetFrame(IndexVector{1})}; |
+ |
+ 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) { |
+ // Focus on input element. |
+ EXPECT_TRUE(ExecuteScript(frames[i], |
+ "window.focus();" |
+ "document.querySelector('input').focus();")); |
+ |
+ // No text selection, set cursor position. |
+ { |
+ TextSelectionObserver selection_observer(active_contents()); |
+ TextInputManagerCursorPositionObserver position_observer( |
+ active_contents(), 2); |
+ EXPECT_TRUE(ExecuteScript( |
+ frames[i], |
+ "document.querySelector('input').setSelectionRange(2, 2);")); |
+ position_observer.Wait(); |
+ // Non-user initiated empty text selection after empty text selection |
+ // should not trigger selection change. |
+ EXPECT_EQ(0, selection_observer.selection_changed_count()); |
+ } |
+ |
+ // Set text selection. |
+ { |
+ TextSelectionObserver selection_observer(active_contents()); |
+ EXPECT_TRUE(ExecuteScript(frames[i], |
+ "document.querySelector('input').select();")); |
+ selection_observer.WaitForSelectedText(values[i], false); |
+ EXPECT_EQ(1, selection_observer.selection_changed_count()); |
+ } |
+ |
+ // Set cursor position and clear text selection. |
+ { |
+ TextSelectionObserver selection_observer(active_contents()); |
+ TextInputManagerCursorPositionObserver position_observer( |
+ active_contents(), 3); |
+ EXPECT_TRUE(ExecuteScript( |
+ frames[i], |
+ "document.querySelector('input').setSelectionRange(3, 3);")); |
+ position_observer.Wait(); |
+ // Non-user initiated empty text selection clears an existing text |
+ // selection. Notification is expected about the selection change. |
+ selection_observer.WaitForSelectedText("", false); |
+ EXPECT_EQ(1, selection_observer.selection_changed_count()); |
+ } |
+ } |
+} |
+ |
// 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). |