// 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);
};
// Tests that the password login is autocompleted as expected when the browser
// sends back the password info.
TEST_F(PasswordAutocompleteManagerTest, 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, 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, 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, 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, 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, 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