Commit 1421e630 authored by Parastoo Geranmayeh's avatar Parastoo Geranmayeh Committed by Commit Bot

[Autofill] Dynamic Form: update the triggering element.

If the triggering element is no longer valid, update it, before
checking if a triggering a refill is necessary. While updating the
old element, check for nameless forms and unowned elements as well.
Also, if the triggering element is not filled, triggering a refill
is necessary.
Tests added.

Fixes (Desktop): etsy, staple and ysl.

Bug: 863575
Change-Id: Id9a775a31a345c5c722687df5958c55b662ebbfd
Reviewed-on: https://chromium-review.googlesource.com/1145486Reviewed-by: default avatarSebastien Seguin-Gagnon <sebsg@chromium.org>
Commit-Queue: Parastoo Geranmayeh <parastoog@google.com>
Cr-Commit-Position: refs/heads/master@{#577911}
parent d0d08b85
...@@ -2711,6 +2711,84 @@ IN_PROC_BROWSER_TEST_P(AutofillDynamicFormInteractiveTest, ...@@ -2711,6 +2711,84 @@ IN_PROC_BROWSER_TEST_P(AutofillDynamicFormInteractiveTest,
ExpectFieldValue("phone", "15125551234"); ExpectFieldValue("phone", "15125551234");
} }
// Test that we can autofill forms that dynamically change the element that
// has been clicked on.
IN_PROC_BROWSER_TEST_P(AutofillDynamicFormInteractiveTest,
DynamicFormFill_FirstElementDisappears) {
CreateTestProfile();
GURL url = embedded_test_server()->GetURL(
"a.com", "/autofill/dynamic_form_element_invalid.html");
ASSERT_NO_FATAL_FAILURE(ui_test_utils::NavigateToURL(browser(), url));
TriggerFormFill("firstname");
// Wait for the re-fill to happen.
bool has_refilled = false;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
GetWebContents(), "hasRefilled()", &has_refilled));
ASSERT_TRUE(has_refilled);
// Make sure the new form was filled correctly.
ExpectFieldValue("firstname2", "Milton");
ExpectFieldValue("address1", "4120 Freidrich Lane");
ExpectFieldValue("city", "Austin");
ExpectFieldValue("company", "Initech");
ExpectFieldValue("email", "red.swingline@initech.com");
ExpectFieldValue("phone", "15125551234");
}
// Test that we can autofill forms that dynamically change the element that
// has been clicked on, even though the form has no name.
IN_PROC_BROWSER_TEST_P(AutofillDynamicFormInteractiveTest,
DynamicFormFill_FirstElementDisappearsNoNameForm) {
CreateTestProfile();
GURL url = embedded_test_server()->GetURL(
"a.com", "/autofill/dynamic_form_element_invalid_noname_form.html");
ASSERT_NO_FATAL_FAILURE(ui_test_utils::NavigateToURL(browser(), url));
TriggerFormFill("firstname");
// Wait for the re-fill to happen.
bool has_refilled = false;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
GetWebContents(), "hasRefilled()", &has_refilled));
ASSERT_TRUE(has_refilled);
// Make sure the new form was filled correctly.
ExpectFieldValue("firstname2", "Milton");
ExpectFieldValue("address1", "4120 Freidrich Lane");
ExpectFieldValue("city", "Austin");
ExpectFieldValue("company", "Initech");
ExpectFieldValue("email", "red.swingline@initech.com");
ExpectFieldValue("phone", "15125551234");
}
// Test that we can autofill forms that dynamically change the element that
// has been clicked on, even though the elements are unowned.
IN_PROC_BROWSER_TEST_P(AutofillDynamicFormInteractiveTest,
DynamicFormFill_FirstElementDisappearsUnowned) {
CreateTestProfile();
GURL url = embedded_test_server()->GetURL(
"a.com", "/autofill/dynamic_form_element_invalid_unowned.html");
ASSERT_NO_FATAL_FAILURE(ui_test_utils::NavigateToURL(browser(), url));
TriggerFormFill("firstname");
// Wait for the re-fill to happen.
bool has_refilled = false;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
GetWebContents(), "hasRefilled()", &has_refilled));
ASSERT_TRUE(has_refilled);
// Make sure the new form was filled correctly.
ExpectFieldValue("firstname2", "Milton");
ExpectFieldValue("address1", "4120 Freidrich Lane");
ExpectFieldValue("city", "Austin");
ExpectFieldValue("company", "Initech");
ExpectFieldValue("email", "red.swingline@initech.com");
ExpectFieldValue("phone", "15125551234");
}
// Test that credit card fields are never re-filled. // Test that credit card fields are never re-filled.
IN_PROC_BROWSER_TEST_P(AutofillDynamicFormInteractiveTest, IN_PROC_BROWSER_TEST_P(AutofillDynamicFormInteractiveTest,
DynamicChangingFormFill_NotForCreditCard) { DynamicChangingFormFill_NotForCreditCard) {
......
<!-- A page that is used to test that a dynamic form fill feature works properly. -->
<body>
<form name="addr1.1" id="form1" action="https://example.com/" method="post">
Name: <input type="text" name="firstname" id="firstname" autocomplete="given-name"><br>
<input type="text" name="firstname" id="firstname2" autocomplete="given-name" style="display: none"><br>
Address: <input type="text" name="address1" id="address1"><br>
City: <input type="text" name="city" id="city"><br>
<input type="text" name="state_us" id="state_us" style="display: none;" autocomplete="region"><br>
Zip: <input name="zip" id="zip"> <br>
Country: <select name="country" id="country" onchange="CountryChanged()">
<option value="CA">Canada</option>
<option value="US">United States</option>
</select> <br>
Company: <input name="company" id="company"> <br>
Email: <input name="email" id="email"> <br>
Phone: <input name="phone" id="phone"> <br>
<input type="reset" value="Reset">
<input type="submit" value="Submit" id="profile_submit">
</form>
</body>
<script>
var notify_on_address_input_change = false;
var address_input_changed = false;
function CountryChanged() {
// Reset the value of the address field.
var address1 = document.getElementById('address1');
address1.value = '';
address1.onchange = function() {
if (notify_on_address_input_change)
window.domAutomationController.send(address1.value != '');
else
address_input_changed = true;
}
// Change the element that triggered the autofill. Remove it, and make another
// field visible. This is to test if the autofill can handle the case where
// the clicked on element is no longer valid.
var first_name_input = document.getElementById("firstname");
first_name_input.parentNode.removeChild(first_name_input);
var name_later = document.getElementById("firstname2");
name_later.removeAttribute('style');
}
function hasRefilled() {
var address1 = document.getElementById('address1');
if (address1 && address_input_changed) {
window.domAutomationController.send(address1.value != '');
} else {
notify_on_address_input_change = true;
}
}
</script>
<!-- A page that is used to test that a dynamic form fill feature works properly. -->
<body>
<form id="form1" action="https://example.com/" method="post">
Name: <input type="text" name="firstname" id="firstname" autocomplete="given-name"><br>
<input type="text" name="firstname" id="firstname2" autocomplete="given-name" style="display: none"><br>
Address: <input type="text" name="address1" id="address1"><br>
City: <input type="text" name="city" id="city"><br>
<input type="text" name="state_us" id="state_us" style="display: none;" autocomplete="region"><br>
Zip: <input name="zip" id="zip"> <br>
Country: <select name="country" id="country" onchange="CountryChanged()">
<option value="CA">Canada</option>
<option value="US">United States</option>
</select> <br>
Company: <input name="company" id="company"> <br>
Email: <input name="email" id="email"> <br>
Phone: <input name="phone" id="phone"> <br>
<input type="reset" value="Reset">
<input type="submit" value="Submit" id="profile_submit">
</form>
</body>
<script>
var notify_on_address_input_change = false;
var address_input_changed = false;
function CountryChanged() {
// Reset the value of the address field.
var address1 = document.getElementById('address1');
address1.value = '';
address1.onchange = function() {
if (notify_on_address_input_change)
window.domAutomationController.send(address1.value != '');
else
address_input_changed = true;
}
// Change the element that triggered the autofill. Remove it, and make another
// field visible. This is to test if the autofill can handle the case where
// the clicked on element is no longer valid.
var first_name_input = document.getElementById("firstname");
first_name_input.parentNode.removeChild(first_name_input);
var name_later = document.getElementById("firstname2");
name_later.removeAttribute('style');
}
function hasRefilled() {
var address1 = document.getElementById('address1');
if (address1 && address_input_changed) {
window.domAutomationController.send(address1.value != '');
} else {
notify_on_address_input_change = true;
}
}
</script>
<!-- A page that is used to test that a dynamic form fill feature works properly. -->
<body>
Name:
<input type="text" name="firstname" id="firstname0" autocomplete="given-name" style="display: none"><br>
<input type="text" name="firstname" id="firstname" autocomplete="given-name"><br>
<input type="text" name="firstname" id="firstname2" autocomplete="given-name" style="display: none"><br>
Address: <input type="text" name="address1" id="address1"><br>
City: <input type="text" name="city" id="city"><br>
<input type="text" name="state_us" id="state_us" style="display: none;" autocomplete="region"><br>
Zip: <input name="zip" id="zip"> <br>
Country: <select name="country" id="country" onchange="CountryChanged()">
<option value="CA">Canada</option>
<option value="US">United States</option>
</select> <br>
Company: <input name="company" id="company"> <br>
Email: <input name="email" id="email"> <br>
Phone: <input name="phone" id="phone"> <br>
<input type="reset" value="Reset">
<input type="submit" value="Submit" id="profile_submit">
</body>
<script>
var notify_on_address_input_change = false;
var address_input_changed = false;
function CountryChanged() {
// Reset the value of the address field.
var address1 = document.getElementById('address1');
address1.value = '';
address1.onchange = function() {
if (notify_on_address_input_change)
window.domAutomationController.send(address1.value != '');
else
address_input_changed = true;
}
// Change the element that triggered the autofill. Remove it, and make another
// field visible. This is to test if the autofill can handle the case where
// the clicked on element is no longer valid.
var first_name_input = document.getElementById("firstname");
first_name_input.parentNode.removeChild(first_name_input);
var name_later = document.getElementById("firstname2");
name_later.removeAttribute('style');
}
function hasRefilled() {
var address1 = document.getElementById('address1');
if (address1 && address_input_changed) {
window.domAutomationController.send(address1.value != '');
} else {
notify_on_address_input_change = true;
}
}
</script>
...@@ -414,18 +414,22 @@ void AutofillAgent::DoAcceptDataListSuggestion( ...@@ -414,18 +414,22 @@ void AutofillAgent::DoAcceptDataListSuggestion(
void AutofillAgent::TriggerRefillIfNeeded(const FormData& form) { void AutofillAgent::TriggerRefillIfNeeded(const FormData& form) {
if (!base::FeatureList::IsEnabled(features::kAutofillDynamicForms)) if (!base::FeatureList::IsEnabled(features::kAutofillDynamicForms))
return; return;
ReplaceElementIfNowInvalid(form);
FormFieldData field; FormFieldData field;
FormData updated_form; FormData updated_form;
if (form_util::FindFormAndFieldForFormControlElement(element_, &updated_form, if (form_util::FindFormAndFieldForFormControlElement(element_, &updated_form,
&field) && &field) &&
!form.DynamicallySameFormAs(updated_form)) { (!element_.IsAutofilled() || !form.DynamicallySameFormAs(updated_form))) {
base::TimeTicks forms_seen_timestamp = base::TimeTicks::Now(); base::TimeTicks forms_seen_timestamp = base::TimeTicks::Now();
WebLocalFrame* frame = render_frame()->GetWebFrame(); WebLocalFrame* frame = render_frame()->GetWebFrame();
std::vector<FormData> forms; std::vector<FormData> forms;
forms.push_back(updated_form); forms.push_back(updated_form);
// Always communicate to browser process for topmost frame. // Always communicate to browser process for topmost frame.
if (!forms.empty() || !frame->Parent()) if (!forms.empty() || !frame->Parent()) {
GetAutofillDriver()->FormsSeen(forms, forms_seen_timestamp); GetAutofillDriver()->FormsSeen(forms, forms_seen_timestamp);
}
} }
} }
...@@ -1026,11 +1030,62 @@ void AutofillAgent::OnFormNoLongerSubmittable() { ...@@ -1026,11 +1030,62 @@ void AutofillAgent::OnFormNoLongerSubmittable() {
submitted_forms_.clear(); submitted_forms_.clear();
} }
bool AutofillAgent::FindTheUniqueNewVersionOfOldElement(
WebVector<WebFormControlElement>& elements,
bool& element_found,
const WebString& original_element_section,
const WebFormControlElement& original_element) {
for (const WebFormControlElement& current_element : elements) {
if (current_element.IsFocusable() &&
original_element.NameForAutofill() ==
current_element.NameForAutofill()) {
if (!element_found) {
element_ = current_element;
element_found = true;
} else if (current_element.AutofillSection() ==
element_.AutofillSection() ||
(current_element.AutofillSection() !=
original_element_section &&
element_.AutofillSection() != original_element_section)) {
// If there are two elements that share the same name with the element_,
// and the section can't tell them apart, we can't decide between the
// two.
element_ = original_element;
return false;
} else if (current_element.AutofillSection() ==
original_element_section) {
// If the current element has the right section, update the element_.
element_ = current_element;
}
}
}
return true;
}
void AutofillAgent::ReplaceElementIfNowInvalid(const FormData& original_form) { void AutofillAgent::ReplaceElementIfNowInvalid(const FormData& original_form) {
// If the document is invalid, bail out. // If the document is invalid, bail out.
if (element_.GetDocument().IsNull()) if (element_.GetDocument().IsNull())
return; return;
WebVector<WebFormElement> forms;
WebVector<WebFormControlElement> elements;
if (original_form.name.empty()) {
// If the form has no name, check all the forms.
bool element_found = false;
element_.GetDocument().Forms(forms);
for (const WebFormElement& form : forms) {
form.GetFormControlElements(elements);
// If finding a unique element is impossible, return.
if (!FindTheUniqueNewVersionOfOldElement(
elements, element_found, element_.AutofillSection(), element_))
return;
}
// If the element is not found, we should still check for unowned elements.
if (element_found)
return;
}
if (!element_.Form().IsNull()) { if (!element_.Form().IsNull()) {
// If |element_|'s parent form has no elements, |element_| is now invalid // If |element_|'s parent form has no elements, |element_| is now invalid
// and should be updated. // and should be updated.
...@@ -1040,29 +1095,32 @@ void AutofillAgent::ReplaceElementIfNowInvalid(const FormData& original_form) { ...@@ -1040,29 +1095,32 @@ void AutofillAgent::ReplaceElementIfNowInvalid(const FormData& original_form) {
return; return;
} }
// Try to find the new version of the form.
WebFormElement form_element; WebFormElement form_element;
WebVector<WebFormElement> forms; if (!original_form.name.empty()) {
element_.GetDocument().Forms(forms); // Try to find the new version of the form.
for (const WebFormElement& form : forms) { element_.GetDocument().Forms(forms);
if (original_form.name == form.GetName().Utf16() || for (const WebFormElement& form : forms) {
original_form.name == form.GetAttribute("id").Utf16()) { if (original_form.name == form.GetName().Utf16() ||
form_element = form; original_form.name == form.GetAttribute("id").Utf16()) {
break; form_element = form;
break;
}
} }
} }
WebVector<WebFormControlElement> elements;
if (form_element.IsNull()) { if (form_element.IsNull()) {
// Could not find the new version of the form, get all the unowned elements. // Could not find the new version of the form, get all the unowned elements.
std::vector<WebElement> fieldsets; std::vector<WebElement> fieldsets;
elements = form_util::GetUnownedAutofillableFormFieldElements( elements = form_util::GetUnownedAutofillableFormFieldElements(
element_.GetDocument().All(), &fieldsets); element_.GetDocument().All(), &fieldsets);
} else { bool element_found = false;
// Get all the elements of the new version of the form. FindTheUniqueNewVersionOfOldElement(elements, element_found,
form_element.GetFormControlElements(elements); element_.AutofillSection(), element_);
return;
} }
// This is the case for owned fields that belong to the right named form.
// Get all the elements of the new version of the form.
form_element.GetFormControlElements(elements);
// Try to find the new version of the last interacted element. // Try to find the new version of the last interacted element.
for (const WebFormControlElement& element : elements) { for (const WebFormControlElement& element : elements) {
if (element_.NameForAutofill() == element.NameForAutofill()) { if (element_.NameForAutofill() == element.NameForAutofill()) {
......
...@@ -29,6 +29,10 @@ ...@@ -29,6 +29,10 @@
namespace blink { namespace blink {
class WebNode; class WebNode;
class WebView; class WebView;
class WebString;
class WebFormControlElement;
template <typename T>
class WebVector;
} }
namespace autofill { namespace autofill {
...@@ -266,6 +270,15 @@ class AutofillAgent : public content::RenderFrameObserver, ...@@ -266,6 +270,15 @@ class AutofillAgent : public content::RenderFrameObserver,
// cleared in this method. // cleared in this method.
void OnFormNoLongerSubmittable(); void OnFormNoLongerSubmittable();
// For no name forms, and unowned elements, try to see if there is a unique
// element in the updated form that corresponds to the old |element_|.
// Returns false if more than one element matches the |element_|.
bool FindTheUniqueNewVersionOfOldElement(
blink::WebVector<blink::WebFormControlElement>& elements,
bool& element_found,
const blink::WebString& original_element_section,
const blink::WebFormControlElement& original_element);
// Check whether |element_| was removed or replaced dynamically on the page. // Check whether |element_| was removed or replaced dynamically on the page.
// If so, looks for the same element in the updated |form| and replaces the // If so, looks for the same element in the updated |form| and replaces the
// |element_| with it if it's found. // |element_| with it if it's found.
......
...@@ -1965,7 +1965,12 @@ void AutofillManager::TriggerRefill(const FormData& form, ...@@ -1965,7 +1965,12 @@ void AutofillManager::TriggerRefill(const FormData& form,
DCHECK(itr != filling_contexts_map_.end()); DCHECK(itr != filling_contexts_map_.end());
FillingContext* filling_context = itr->second.get(); FillingContext* filling_context = itr->second.get();
DCHECK(!filling_context->attempted_refill); // The refill attempt can happen from different paths, some of which happen
// after waiting for a while. Therefore, although this condition has been
// checked prior to calling TriggerRefill, it may not hold, when we get here.
if (filling_context->attempted_refill)
return;
filling_context->attempted_refill = true; filling_context->attempted_refill = true;
// Try to find the field from which the original field originated. // Try to find the field from which the original field originated.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment