// Copyright (c) 2010 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 "app/keyboard_codes.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" #include "chrome/renderer/password_autocomplete_manager.h" #include "chrome/test/render_view_test.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/WebKit/WebKit/chromium/public/WebDocument.h" #include "third_party/WebKit/WebKit/chromium/public/WebElement.h" #include "third_party/WebKit/WebKit/chromium/public/WebFormElement.h" #include "third_party/WebKit/WebKit/chromium/public/WebInputElement.h" #include "third_party/WebKit/WebKit/chromium/public/WebNode.h" #include "third_party/WebKit/WebKit/chromium/public/WebString.h" #include "third_party/WebKit/WebKit/chromium/public/WebVector.h" #include "webkit/glue/form_data.h" #include "webkit/glue/form_field.h" using webkit_glue::FormField; using webkit_glue::PasswordFormFillData; using webkit_glue::PasswordForm; using webkit_glue::PasswordFormDomManager; using WebKit::WebDocument; using WebKit::WebElement; using WebKit::WebFrame; using WebKit::WebInputElement; using WebKit::WebString; namespace { // The name of the username/password element in the form. const char* const kUsernameName = "username"; const char* const kPasswordName = "password"; const char* const kAliceUsername = "alice"; const char* const kAlicePassword = "password"; const char* const kBobUsername = "bob"; const char* const kBobPassword = "secret"; const char* const kFormHTML = "
" " " " " " " "
"; class PasswordAutocompleteManagerTest : public RenderViewTest { public: PasswordAutocompleteManagerTest() { } // Simulates the fill password form message being sent to the renderer. // We use that so we don't have to make RenderView::OnFillPasswordForm() // protected. void SimulateOnFillPasswordForm( const PasswordFormFillData& fill_data) { ViewMsg_FillPasswordForm msg(0, fill_data); view_->OnMessageReceived(msg); } virtual void SetUp() { RenderViewTest::SetUp(); // Add a preferred login and an additional login to the FillData. username1_ = ASCIIToUTF16(kAliceUsername); password1_ = ASCIIToUTF16(kAlicePassword); username2_ = ASCIIToUTF16(kBobUsername); password2_ = ASCIIToUTF16(kBobPassword); fill_data_.basic_data.fields.push_back( FormField(string16(), ASCIIToUTF16(kUsernameName), username1_, string16(), 0)); fill_data_.basic_data.fields.push_back( FormField(string16(), ASCIIToUTF16(kPasswordName), password1_, string16(), 0)); fill_data_.additional_logins[username2_] = password2_; // We need to set the origin so it matches the frame URL and the action so // it matches the form action, otherwise we won't autocomplete. std::string origin("data:text/html;charset=utf-8,"); origin += kFormHTML; fill_data_.basic_data.origin = GURL(origin); fill_data_.basic_data.action = GURL("http://www.bidule.com"); LoadHTML(kFormHTML); // Now retrieves the input elements so the test can access them. WebDocument document = GetMainFrame()->document(); WebElement element = document.getElementById(WebString::fromUTF8(kUsernameName)); ASSERT_FALSE(element.isNull()); username_element_ = element.to(); element = document.getElementById(WebString::fromUTF8(kPasswordName)); ASSERT_FALSE(element.isNull()); password_element_ = element.to(); } void ClearUsernameAndPasswordFields() { username_element_.setValue(""); username_element_.setAutofilled(false); password_element_.setValue(""); password_element_.setAutofilled(false); } void SimulateUsernameChange(const std::string& username, bool move_caret_to_end) { username_element_.setValue(WebString::fromUTF8(username)); if (move_caret_to_end) username_element_.setSelectionRange(username.length(), username.length()); view_->textFieldDidChange(username_element_); // Processing is delayed because of a WebKit bug, see // PasswordAutocompleteManager::TextDidChangeInTextField() for details. MessageLoop::current()->RunAllPending(); } void SimulateKeyDownEvent(const WebInputElement& element, app::KeyboardCode key_code) { WebKit::WebKeyboardEvent key_event; key_event.windowsKeyCode = key_code; view_->textFieldDidReceiveKeyDown(element, key_event); } void CheckTextFieldsState(const std::string& username, bool username_autofilled, const std::string& password, bool password_autofilled) { EXPECT_EQ(username, static_cast(username_element_.value().utf8())); EXPECT_EQ(username_autofilled, username_element_.isAutofilled()); EXPECT_EQ(password, static_cast(password_element_.value().utf8())); EXPECT_EQ(password_autofilled, password_element_.isAutofilled()); } void CheckUsernameSelection(int start, int end) { EXPECT_EQ(start, username_element_.selectionStart()); EXPECT_EQ(end, username_element_.selectionEnd()); } string16 username1_; string16 username2_; string16 password1_; string16 password2_; PasswordFormFillData fill_data_; WebInputElement username_element_; WebInputElement password_element_; private: DISALLOW_COPY_AND_ASSIGN(PasswordAutocompleteManagerTest); }; #if defined(WEBKIT_BUG_41283_IS_FIXED) #define MAYBE_InitialAutocomplete InitialAutocomplete #define MAYBE_NoInitialAutocompleteForReadOnly NoInitialAutocompleteForReadOnly #define MAYBE_PasswordClearOnEdit PasswordClearOnEdit #define MAYBE_WaitUsername WaitUsername #define MAYBE_InlineAutocomplete InlineAutocomplete #define MAYBE_SuggestionSelect SuggestionSelect #else #define MAYBE_InitialAutocomplete DISABLED_InitialAutocomplete #define MAYBE_NoInitialAutocompleteForReadOnly \ DISABLED_NoInitialAutocompleteForReadOnly #define MAYBE_PasswordClearOnEdit DISABLED_PasswordClearOnEdit #define MAYBE_WaitUsername DISABLED_WaitUsername #define MAYBE_InlineAutocomplete DISABLED_InlineAutocomplete #define MAYBE_SuggestionSelect DISABLED_SuggestionSelect #endif // Tests that the password login is autocompleted as expected when the browser // sends back the password info. TEST_F(PasswordAutocompleteManagerTest, MAYBE_InitialAutocomplete) { /* * Right now we are not sending the message to the browser because we are * loading a data URL and the security origin canAccessPasswordManager() * returns false. May be we should mock URL loading to cirmcuvent this? TODO(jcivelli): find a way to make the security origin not deny access to the password manager and then reenable this code. // The form has been loaded, we should have sent the browser a message about // the form. const IPC::Message* msg = render_thread_.sink().GetFirstMessageMatching( ViewHostMsg_PasswordFormsFound::ID); ASSERT_TRUE(msg != NULL); Tuple1 > forms; ViewHostMsg_PasswordFormsFound::Read(msg, &forms); ASSERT_EQ(1U, forms.a.size()); PasswordForm password_form = forms.a[0]; EXPECT_EQ(PasswordForm::SCHEME_HTML, password_form.scheme); EXPECT_EQ(ASCIIToUTF16(kUsernameName), password_form.username_element); EXPECT_EQ(ASCIIToUTF16(kPasswordName), password_form.password_element); */ // Simulate the browser sending back the login info, it triggers the // autocomplete. SimulateOnFillPasswordForm(fill_data_); // The username and password should have been autocompleted. CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); } // Tests that changing the username does not fill a read-only password field. TEST_F(PasswordAutocompleteManagerTest, MAYBE_NoInitialAutocompleteForReadOnly) { password_element_.setAttribute(WebString::fromUTF8("readonly"), WebString::fromUTF8("true")); // Simulate the browser sending back the login info, it triggers the // autocompleted. SimulateOnFillPasswordForm(fill_data_); // Only the username should have been autocompleted. // TODO(jcivelli): may be we should not event fill the username? CheckTextFieldsState(kAliceUsername, true, "", false); } // Tests that editing the password clears the autocompleted password field. TEST_F(PasswordAutocompleteManagerTest, MAYBE_PasswordClearOnEdit) { // Simulate the browser sending back the login info, it triggers the // autocomplete. SimulateOnFillPasswordForm(fill_data_); // Simulate the user changing the username to some unknown username. SimulateUsernameChange("alicia", true); // The password should have been cleared. CheckTextFieldsState("alicia", false, "", false); } // Tests that we only autocomplete on focus lost and with a full username match // when |wait_for_username| is true. TEST_F(PasswordAutocompleteManagerTest, MAYBE_WaitUsername) { // Simulate the browser sending back the login info. fill_data_.wait_for_username = true; SimulateOnFillPasswordForm(fill_data_); // No auto-fill should have taken place. CheckTextFieldsState("", false, "", false); // No autocomplete should happen when text is entered in the username. SimulateUsernameChange("a", true); CheckTextFieldsState("a", false, "", false); SimulateUsernameChange("al", true); CheckTextFieldsState("al", false, "", false); SimulateUsernameChange(kAliceUsername, true); CheckTextFieldsState(kAliceUsername, false, "", false); // Autocomplete should happen only when the username textfield is blurred with // a full match. username_element_.setValue("a"); view_->textFieldDidEndEditing(username_element_); CheckTextFieldsState("a", false, "", false); username_element_.setValue("al"); view_->textFieldDidEndEditing(username_element_); CheckTextFieldsState("al", false, "", false); username_element_.setValue("alices"); view_->textFieldDidEndEditing(username_element_); CheckTextFieldsState("alices", false, "", false); username_element_.setValue(ASCIIToUTF16(kAliceUsername)); view_->textFieldDidEndEditing(username_element_); CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); } // Tests that inline autocompletion works properly. TEST_F(PasswordAutocompleteManagerTest, MAYBE_InlineAutocomplete) { // Simulate the browser sending back the login info. SimulateOnFillPasswordForm(fill_data_); // Clear the textfields to start fresh. ClearUsernameAndPasswordFields(); // Simulate the user typing in the first letter of 'alice', a stored username. SimulateUsernameChange("a", true); // Both the username and password textfields should reflect selection of the // stored login. CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); // And the selection should have been set to 'lice', the last 4 letters. CheckUsernameSelection(1, 5); // Now the user types the next letter of the same username, 'l'. SimulateUsernameChange("al", true); // Now the fields should have the same value, but the selection should have a // different start value. CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); CheckUsernameSelection(2, 5); // Test that deleting does not trigger autocomplete. SimulateKeyDownEvent(username_element_, app::VKEY_BACK); SimulateUsernameChange("alic", true); CheckTextFieldsState("alic", false, "", false); CheckUsernameSelection(4, 4); // No selection. // Reset the last pressed key to something other than backspace. SimulateKeyDownEvent(username_element_, app::VKEY_A); // Now lets say the user goes astray from the stored username and types the // letter 'f', spelling 'alf'. We don't know alf (that's just sad), so in // practice the username should no longer be 'alice' and the selected range // should be empty. SimulateUsernameChange("alf", true); CheckTextFieldsState("alf", false, "", false); CheckUsernameSelection(3, 3); // No selection. // Ok, so now the user removes all the text and enters the letter 'b'. SimulateUsernameChange("b", true); // The username and password fields should match the 'bob' entry. CheckTextFieldsState(kBobUsername, true, kBobPassword, true); CheckUsernameSelection(1, 3); } // Tests that selecting and item in the suggestion drop-down works. TEST_F(PasswordAutocompleteManagerTest, MAYBE_SuggestionSelect) { // Simulate the browser sending back the login info. SimulateOnFillPasswordForm(fill_data_); // Clear the textfields to start fresh. ClearUsernameAndPasswordFields(); // To simulate a selection in the suggestion drop-down we just mimick what the // WebView does: it sets the element value then calls // didAcceptAutocompleteSuggestion on the renderer. username_element_.setValue(ASCIIToUTF16(kAliceUsername)); view_->didAcceptAutocompleteSuggestion(username_element_); // Autocomplete should have kicked in. CheckTextFieldsState(kAliceUsername, true, kAlicePassword, true); } } // namespace