blob: 53c2a1a2e4fa01e003216e658ccdb888f6606d56 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/autofill/content/renderer/form_autofill_util.h"
#include <variant>
#include <vector>
#include "base/feature_list.h"
#include "base/metrics/field_trial.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/autofill/content/renderer/synchronous_form_cache.h"
#include "components/autofill/content/renderer/test_utils.h"
#include "components/autofill/core/common/autofill_constants.h"
#include "components/autofill/core/common/autofill_data_validation.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/field_data_manager.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/form_field_data.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#include "components/autofill/core/common/unique_ids.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/test/render_view_test.h"
#include "content/public/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_element_collection.h"
#include "third_party/blink/public/web/web_form_control_element.h"
#include "third_party/blink/public/web/web_form_element.h"
#include "third_party/blink/public/web/web_input_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_remote_frame.h"
#include "third_party/blink/public/web/web_select_element.h"
#include "third_party/blink/public/web/web_view.h"
namespace autofill::form_util {
namespace {
using ::autofill::mojom::ButtonTitleType;
using ::blink::WebDocument;
using ::blink::WebElement;
using ::blink::WebElementCollection;
using ::blink::WebFormControlElement;
using ::blink::WebFormElement;
using ::blink::WebInputElement;
using ::blink::WebLocalFrame;
using ::blink::WebNode;
using ::blink::WebString;
using ::testing::_;
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::IsFalse;
using ::testing::IsTrue;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::Pointwise;
using ::testing::Property;
using ::testing::Values;
struct AutofillFieldUtilCase {
std::string_view description;
std::string_view html;
std::u16string_view expected_label;
};
// An <input> with a label placed on top of it (usually used as a placeholder
// replacement).
const char* kPoorMansPlaceholderFullOverlap = R"(
<style>
.fixed_position_and_size {
position: fixed;
top: 0;
left: 0;
width: 100px;
height: 20px;
}
</style>
<input id=target class=fixed_position_and_size>
<span class=fixed_position_and_size>label</span>
)";
// The <input> element partially overlaps the label (placeholder) but the label
// is not fully contained in the <input> element. This is a common case for
// placeholders that moph into a minified version when the user focuses an
// <input> element.
const char* kPoorMansPlaceholderPartialOverlap = R"(
<style>
.fixed_position_and_size {
position: fixed;
top: 30px;
left: 0;
width: 100px;
height: 20px;
}
.overlapping_position_and_size {
position: fixed;
top: 25px;
left: 0;
width: 100px;
height: 20px;
}
</style>
<input id=target class=fixed_position_and_size>
<span class=overlapping_position_and_size>label</span>
)";
// The <input> element touches the next element vertically but does not overlap.
// The label should not be considered a placeholder.
const char* kPoorMansPlaceholderNoOverlap = R"(
<input id='target'>
<div>not a label</div>
)";
// The <input> element touches the next element horizontally but does not
// overlap. The label should not be considered a placeholder.
const char* kPoorMansPlaceholderNoOverlap2 = R"(
<input id=target>
<span>not a label</span>
)";
// The span exceeds the vertical limits of the input element, which is a
// pattern often observed in error messages. Therefore we don't consider the
// span a label.
const char* kPoorMansPlaceholderPossiblyErrorMessage = R"(
<style>
.fixed_position_and_size {
position: fixed;
top: 0px;
left: 0;
width: 100px;
height: 20px;
}
.label_position_and_size {
position: fixed;
top: 15px;
left: 0;
width: 100px;
height: 25px;
}
</style>
<input id=target class=fixed_position_and_size>
<span class=overlapping_position_and_size>not a label</span>
)";
// The span is not horizontally contained in the input element. We don't
// consider this a label because have seen several cases where the actual
// label was on the left of the input field in a <table> structure and the
// text on the right, which just touched the element contained non-label
// data (e.g. instructions like "don't enter symbols").
const char* kPoorMansPlaceholderNoHorizontalContainment = R"(
<style>
.fixed_position_and_size {
position: fixed;
top: 0px;
left: 0;
width: 100px;
height: 20px;
}
.label_position_and_size {
position: fixed;
top: 15px;
left: 90px;
width: 100px;
height: 20px;
}
</style>
<input id=target class=fixed_position_and_size>
<span class=overlapping_position_and_size>not a label</span>
)";
auto HasRendererIdOf(const WebFormElement& e) {
return Property("FormData::renderer_id()", &FormData::renderer_id,
GetFormRendererId(e));
}
auto HasRendererIdOf(const WebFormControlElement& e) {
return Property("FormFieldData::renderer_id()", &FormFieldData::renderer_id,
GetFieldRendererId(e));
}
auto FormControlTypesAre(auto&&... form_control_types) {
return ElementsAre(Property("form_control_type",
&FormFieldData::form_control_type,
form_control_types)...);
}
void VerifyButtonTitleCache(const WebFormElement& form_target,
const ButtonTitleList& expected_button_titles,
const ButtonTitlesCache& actual_cache) {
EXPECT_THAT(actual_cache, ElementsAre(Pair(GetFormRendererId(form_target),
expected_button_titles)));
}
bool HaveSameFormControlId(const WebFormControlElement& element,
const FormFieldData& field) {
return GetFieldRendererId(element) == field.renderer_id();
}
class FormAutofillUtilsTest : public content::RenderViewTest {
public:
static constexpr CallTimerState kCallTimerStateDummy = {
.call_site = CallTimerState::CallSite::kUpdateFormCache,
.last_autofill_agent_reset = {},
.last_dom_content_loaded = {},
};
FormAutofillUtilsTest() {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/
{features::kAutofillReplaceCachedWebElementsByRendererIds,
features::kAutofillIgnoreCheckableElements},
/*disabled_features=*/{});
}
~FormAutofillUtilsTest() override = default;
WebDocument GetDocument() { return GetMainFrame()->GetDocument(); }
std::optional<FormData> ExtractFormData(WebFormElement form) {
return form_util::ExtractFormData(GetDocument(), form, field_data_manager(),
kCallTimerStateDummy,
/*button_titles_cache=*/nullptr);
}
std::optional<std::pair<FormData, raw_ref<const FormFieldData>>>
FindFormAndFieldForFormControlElement(WebFormControlElement control) {
return form_util::FindFormAndFieldForFormControlElement(
control, field_data_manager(), kCallTimerStateDummy,
/*button_titles_cache=*/nullptr,
/*form_cache=*/{});
}
FieldDataManager& field_data_manager() { return *field_data_manager_; }
private:
base::test::ScopedFeatureList scoped_feature_list_;
scoped_refptr<FieldDataManager> field_data_manager_ =
base::MakeRefCounted<FieldDataManager>();
};
// Tests that some form control types are extracted by ExtractFormData() and
// others are not.
TEST_F(FormAutofillUtilsTest, ExtractFormData_FormControlTypes) {
LoadHTML(R"(
<form id=form-id>
<!-- These form controls are not extracted. -->
<div contenteditable></div>
<button type=kButtonButton>Foo</button>
<button type=kButtonSubmit>Foo</button>
<button type=kButtonReset>Foo</button>
<button type=kButtonPopover>Foo</button>
<fieldset></fieldset>
<input type=button>
<input type=checkbox>
<input type=color>
<input type=datetime-local>
<input type=file>
<input type=hidden>
<input type=image>
<input type=radio>
<input type=range>
<input type=reset>
<input type=submit>
<input type=time>
<input type=week>
<output>Foo</output>
<select multiple><option>Foo</option><option>Bar</option></select>
<!-- These form controls are extracted. -->
<input>
<input type=email>
<input type=month>
<input type=number>
<input type=password>
<input type=search>
<input type=tel>
<input type=text>
<input type=url>
<input type=date>
<select><option>Foo</option><option>Bar</option></select>
<textarea>Foo</textarea>
</form>
)");
FormData form_data =
*ExtractFormData(GetFormElementById(GetDocument(), "form-id"));
using enum FormControlType;
EXPECT_THAT(form_data.fields(),
FormControlTypesAre(kInputText, kInputEmail, kInputMonth,
kInputNumber, kInputPassword, kInputSearch,
kInputTelephone, kInputText, kInputUrl,
kInputDate, kSelectOne, kTextArea));
}
// Tests that WebFormElementToFormData() sets the
// Form[Field]Data::{name,id_attribute,name_attribute} correctly.
TEST_F(FormAutofillUtilsTest, WebFormElementToFormData_IdAndNames) {
LoadHTML(R"(
<form id=form-id name=form-name>
<input type=text id=input-id name=input-name>
</form>
)");
FormData form_data =
*ExtractFormData(GetFormElementById(GetDocument(), "form-id"));
EXPECT_EQ(form_data.name(), u"form-name");
EXPECT_EQ(form_data.id_attribute(), u"form-id");
EXPECT_EQ(form_data.name_attribute(), u"form-name");
ASSERT_EQ(form_data.fields().size(), 1u);
EXPECT_EQ(form_data.fields()[0].name(), u"input-name");
EXPECT_EQ(form_data.fields()[0].id_attribute(), u"input-id");
EXPECT_EQ(form_data.fields()[0].name_attribute(), u"input-name");
}
// Tests that form extraction measures its total time, also split by caller.
TEST_F(FormAutofillUtilsTest, ExtractFormDataMeasuresTotalTime) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<input>
)");
FormData form_data = *ExtractFormData(WebFormElement());
histogram_tester.ExpectTotalCount("Autofill.TimingPrecise.ExtractFormData",
1);
histogram_tester.ExpectTotalCount(
"Autofill.TimingPrecise.ExtractFormData.UpdateFormCache", 1);
histogram_tester.ExpectTotalCount(
"Autofill.TimingInterval.ExtractFormData.UpdateFormCache."
"AutofillAgentReset",
1);
histogram_tester.ExpectTotalCount(
"Autofill.TimingInterval.ExtractFormData.UpdateFormCache."
"DOMContentLoaded",
1);
}
// Tests that form extraction measures how long label extraction took.
TEST_F(FormAutofillUtilsTest,
ExtractFormDataMeasuresDurationOfLabelExtraction) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<form id=form-id>
<input type=text>
</form>
)");
FormData form_data =
*ExtractFormData(GetFormElementById(GetDocument(), "form-id"));
histogram_tester.ExpectTotalCount(
"Autofill.TimingPrecise.InferLabelForElement", 1);
}
// Tests that large option values/contents are truncated while building the
// FormData.
TEST_F(FormAutofillUtilsTest, TruncateLargeOptionValuesAndContents) {
std::string huge_option(kMaxStringLength + 10, 'a');
std::u16string trimmed_option(kMaxStringLength, 'a');
LoadHTML(base::StringPrintf(R"(
<form id='form'>
<select name='form_select' id='form_select'>
<option value='%s'>%s</option>
</select>
</form>
)",
huge_option.c_str(), huge_option.c_str())
.c_str());
auto web_form = GetFormElementById(GetDocument(), "form");
FormData form_data = *ExtractFormData(web_form);
ASSERT_EQ(form_data.fields().size(), 1u);
ASSERT_EQ(form_data.fields()[0].options().size(), 1u);
EXPECT_EQ(form_data.fields()[0].options()[0].value, trimmed_option);
EXPECT_EQ(form_data.fields()[0].options()[0].text, trimmed_option);
EXPECT_TRUE(IsValidOption(form_data.fields()[0].options()[0]));
}
// Tests that the SelectOption::value and SelectOption::text are extracted
// correctly.
TEST_F(FormAutofillUtilsTest, ExtractFormData_SelectOptionValueAndText) {
LoadHTML(R"(
<select>
<option value=V label=L >T</option>
<option value=V >T</option>
<option label=L >T</option>
<option >T</option>
<option value=V ></option>
<option label=L ></option>
<option aria-label=A></option>
</select>
)");
std::optional<FormData> form = ExtractFormData(WebFormElement());
ASSERT_TRUE(form);
EXPECT_THAT(form->fields().front().options(),
ElementsAre(SelectOption{.value = u"V", .text = u"L"},
SelectOption{.value = u"V", .text = u"T"},
SelectOption{.value = u"T", .text = u"L"},
SelectOption{.value = u"T", .text = u"T"},
SelectOption{.value = u"V", .text = u""},
SelectOption{.value = u"", .text = u"L"},
SelectOption{.value = u"", .text = u"A"}));
}
TEST_F(FormAutofillUtilsTest, FindChildTextTest) {
static const AutofillFieldUtilCase test_cases[] = {
{"simple test", "<div id='target'>test</div>", u"test"},
{"Concatenate test", "<div id='target'><span>one</span>two</div>",
u"onetwo"},
// Test that "two" is not inferred, because for the purpose of label
// extraction, we only care about text before the input element.
{"Ignore input", "<div id='target'>one<input value='test'/>two</div>",
u"one"},
{"Trim", "<div id='target'> one<span>two </span></div>", u"onetwo"},
{"eleven children",
"<div id='target'>"
"<div>child0</div>"
"<div>child1</div>"
"<div>child2</div>"
"<div>child3</div>"
"<div>child4</div>"
"<div>child5</div>"
"<div>child6</div>"
"<div>child7</div>"
"<div>child8</div>"
"<div>child9</div>"
"<div>child10</div>",
u"child0child1child2child3child4child5child6child7child8"},
// TODO(crbug.com/40555780): Depth is only 5 elements instead of 10. This
// happens because every div and every text node decrease the depth.
{"eleven children nested",
"<div id='target'>"
"<div>child0"
"<div>child1"
"<div>child2"
"<div>child3"
"<div>child4"
"<div>child5"
"<div>child6"
"<div>child7"
"<div>child8"
"<div>child9"
"<div>child10"
"</div></div></div></div></div></div></div></div></div></div></div></"
"div>",
u"child0child1child2child3child4"},
{"Skip script tags",
"<div id='target'><script>alert('hello');</script>label</div>",
u"label"},
{"Script tag whitespacing",
"<div id='target'>Auto<script>alert('hello');</script>fill</div>",
u"Autofill"}};
for (auto test_case : test_cases) {
SCOPED_TRACE(test_case.description);
LoadHTML(test_case.html);
WebElement target = GetElementById(GetDocument(), "target");
EXPECT_EQ(test_case.expected_label, FindChildText(target));
}
}
TEST_F(FormAutofillUtilsTest, FindChildTextSkipElementTest) {
static const AutofillFieldUtilCase test_cases[] = {
// Test that everything after the "skip" div is discarded.
{"Skip div element", R"(
<div id=target>
<div>child0</div>
<div class=skip>child1</div>
<div>child2</div>
</div>)",
u"child0"},
};
for (auto test_case : test_cases) {
SCOPED_TRACE(test_case.description);
LoadHTML(test_case.html);
WebElement target = GetElementById(GetDocument(), "target");
std::vector<WebElement> web_to_skip =
GetDocument().QuerySelectorAll("div[class='skip']");
std::set<WebNode> to_skip;
for (const WebElement& element : web_to_skip) {
to_skip.insert(element);
}
EXPECT_EQ(test_case.expected_label,
FindChildTextWithIgnoreListForTesting(target, to_skip));
}
}
TEST_F(FormAutofillUtilsTest, InferLabelForElementTest) {
static const AutofillFieldUtilCase test_cases[] = {
{"DIV table test 1", R"(
<div>
<div>label</div><div><input id=target></div>
</div>)",
u"label"},
{"DIV table test 2", R"(
<div>
<div>label</div>
<div>should be skipped<input></div>
<div><input id=target></div>
</div>)",
u"label"},
{"DIV table test 3", R"(
<div>
<div>should be skipped<input></div>
<div>label</div>
<div><input id=target></div>
</div>)",
u"label"},
{"DIV table test 4", R"(
<div>
<div>should be skipped<input></div>
label
<div><input id=target></div>
</div>)",
u"label"},
{"DIV table test 5",
"<div>"
"<div>label<div><input id='target'/></div>behind</div>"
"</div>",
u"label"},
{"DIV table test 6", R"(
<div>
label
<div>*</div>
<div><input id='target'></div>
</div>)",
u"label"},
{"Infer from next sibling",
"<input id='target' type='checkbox'>hello <b>world</b>", u"hello world"},
{"Poor man's placeholder", kPoorMansPlaceholderFullOverlap, u"label"},
{"Poor man's placeholder partial overlap",
kPoorMansPlaceholderPartialOverlap, u"label"},
{"Poor man's placeholder no overlap", kPoorMansPlaceholderNoOverlap, u""},
{"Poor man's placeholder no overlap 2", kPoorMansPlaceholderNoOverlap2,
u""},
{"Poor man's placeholder: possibly an error message",
kPoorMansPlaceholderPossiblyErrorMessage, u""},
{"Poor man's placeholder: no horizontal containment",
kPoorMansPlaceholderNoHorizontalContainment, u""}};
for (auto test_case : test_cases) {
SCOPED_TRACE(test_case.description);
LoadHTML(test_case.html);
WebFormControlElement form_target =
GetFormControlElementById(GetDocument(), "target");
std::vector<FormFieldData> fields(1);
InferLabelForElementsForTesting(
std::to_array<WebFormControlElement>({form_target}), fields);
EXPECT_EQ(fields.front().label(), test_case.expected_label);
}
}
TEST_F(FormAutofillUtilsTest, InferLabelSourceTest) {
struct AutofillFieldLabelSourceCase {
const char* html;
const FormFieldData::LabelSource label_source;
};
static const AutofillFieldLabelSourceCase test_cases[] = {
{"<div><div>label</div><div><input id='target'/></div></div>",
FormFieldData::LabelSource::kDivTable},
{"<label>label</label><input id='target'/>",
FormFieldData::LabelSource::kLabelTag},
{"<b>l</b><strong>a</strong>bel<input id='target'/>",
FormFieldData::LabelSource::kCombined},
{"<p><b>l</b><strong>a</strong>bel</p><input id='target'/>",
FormFieldData::LabelSource::kPTag},
{"<input id='target' placeholder='label'/>",
FormFieldData::LabelSource::kPlaceHolder},
{"<input id='target' aria-label='label'/>",
FormFieldData::LabelSource::kAriaLabel},
{"<input id='target' value='label'/>",
FormFieldData::LabelSource::kValue},
// In the next test, the text node is picked up on the way up the DOM-tree
// by the div extraction logic.
{"<li>label<div><input id='target'/></div></li>",
FormFieldData::LabelSource::kDivTable},
{"<li><span>label</span><div><input id='target'/></div></li>",
FormFieldData::LabelSource::kLiTag},
{"<table><tr><td>label</td><td><input id='target'/></td></tr></table>",
FormFieldData::LabelSource::kTdTag},
{"<dl><dt>label</dt><dd><input id='target'></dd></dl>",
FormFieldData::LabelSource::kDdTag},
{kPoorMansPlaceholderFullOverlap,
FormFieldData::LabelSource::kOverlayingLabel}};
for (auto test_case : test_cases) {
SCOPED_TRACE(testing::Message() << test_case.label_source);
LoadHTML(test_case.html);
WebFormControlElement form_target =
GetFormControlElementById(GetDocument(), "target");
std::vector<FormFieldData> fields(1);
InferLabelForElementsForTesting(
std::to_array<WebFormControlElement>({form_target}), fields);
EXPECT_EQ(fields.front().label_source(), test_case.label_source);
}
}
TEST_F(FormAutofillUtilsTest, GetButtonTitles) {
constexpr char kHtml[] =
"<form id='target'>"
" <input type='button' value='Clear field'>"
" <input type='button' value='Clear field'>"
" <input type='button' value='Clear field'>"
" <input type='button' value='\n Show\t password '>"
" <button>Sign Up</button>"
" <button type='button'>Register</button>"
" <a id='Submit' value='Create account'>"
" <div name='BTN'> Join </div>"
" <span class='button'> Start </span>"
" <a class='empty button' value=' \t \n'>"
"</form>";
LoadHTML(kHtml);
WebFormElement form_target = GetFormElementById(GetDocument(), "target");
ButtonTitlesCache cache;
ButtonTitleList actual = GetButtonTitles(form_target, &cache);
ButtonTitleList expected = {
{u"Sign Up", ButtonTitleType::BUTTON_ELEMENT_SUBMIT_TYPE}};
EXPECT_EQ(expected, actual);
VerifyButtonTitleCache(form_target, expected, cache);
}
TEST_F(FormAutofillUtilsTest, GetButtonTitles_TooLongTitle) {
std::string kFormHtml = "<form id='target'>";
for (int i = 0; i < 10; i++) {
std::string kFieldHtml = "<input type='button' value='" +
base::NumberToString(i) + std::string(300, 'a') +
"'>";
kFormHtml += kFieldHtml;
}
kFormHtml += "</form>";
LoadHTML(kFormHtml.c_str());
WebFormElement form_target = GetFormElementById(GetDocument(), "target");
ButtonTitlesCache cache;
ButtonTitleList actual = GetButtonTitles(form_target, &cache);
int total_length = 0;
for (const auto& [title, title_type] : actual) {
EXPECT_GE(30u, title.length());
total_length += title.length();
}
EXPECT_EQ(200, total_length);
}
TEST_F(FormAutofillUtilsTest, GetButtonTitles_NoCache) {
constexpr char kHtml[] =
"<form id='target'>"
" <input type='button' value='Clear field'>"
" <input type='button' value='Clear field'>"
" <input type='button' value='Clear field'>"
" <input type='button' value='\n Show\t password '>"
" <button>Sign Up</button>"
" <button type='button'>Register</button>"
" <a id='Submit' value='Create account'>"
" <div name='BTN'> Join </div>"
" <span class='button'> Start </span>"
" <a class='empty button' value=' \t \n'>"
"</form>";
LoadHTML(kHtml);
WebFormElement form_target = GetFormElementById(GetDocument(), "target");
ButtonTitleList expected = {
{u"Sign Up", ButtonTitleType::BUTTON_ELEMENT_SUBMIT_TYPE}};
ButtonTitleList actual =
GetButtonTitles(form_target, /*button_titles_cache=*/nullptr);
EXPECT_EQ(expected, actual);
}
TEST_F(FormAutofillUtilsTest, GetButtonTitles_NoForm) {
// Attempting to get button titles from a null form should produce an empty
// list and not crash.
WebFormElement form;
ASSERT_FALSE(form);
EXPECT_EQ(GetButtonTitles(form, /*button_titles_cache=*/nullptr).size(), 0u);
}
TEST_F(FormAutofillUtilsTest, IsEnabled) {
LoadHTML(
"<input type='text' id='name1'>"
"<input type='password' disabled id='name2'>"
"<input type='password' id='name3'>"
"<input type='text' id='name4' disabled>");
std::optional<FormData> form = *ExtractFormData(WebFormElement());
EXPECT_THAT(
form, Optional(Property(
&FormData::fields,
ElementsAre(
AllOf(Property(&FormFieldData::name, u"name1"),
Property(&FormFieldData::is_enabled, IsTrue())),
AllOf(Property(&FormFieldData::name, u"name2"),
Property(&FormFieldData::is_enabled, IsFalse())),
AllOf(Property(&FormFieldData::name, u"name3"),
Property(&FormFieldData::is_enabled, IsTrue())),
AllOf(Property(&FormFieldData::name, u"name4"),
Property(&FormFieldData::is_enabled, IsFalse()))))));
}
TEST_F(FormAutofillUtilsTest, IsReadonly) {
LoadHTML(
"<input type='text' id='name1'>"
"<input readonly type='password' id='name2'>"
"<input type='password' id='name3'>"
"<input type='text' id='name4' readonly>");
std::optional<FormData> form = *ExtractFormData(WebFormElement());
EXPECT_THAT(
form, Optional(Property(
&FormData::fields,
ElementsAre(
AllOf(Property(&FormFieldData::name, u"name1"),
Property(&FormFieldData::is_readonly, IsFalse())),
AllOf(Property(&FormFieldData::name, u"name2"),
Property(&FormFieldData::is_readonly, IsTrue())),
AllOf(Property(&FormFieldData::name, u"name3"),
Property(&FormFieldData::is_readonly, IsFalse())),
AllOf(Property(&FormFieldData::name, u"name4"),
Property(&FormFieldData::is_readonly, IsTrue()))))));
}
TEST_F(FormAutofillUtilsTest, IsFocusable) {
LoadHTML(
"<input type='text' id='name1' value='123'>"
"<input type='text' id='name2' style='display:none'>");
std::optional<FormData> form = *ExtractFormData(WebFormElement());
EXPECT_THAT(
form,
Optional(Property(
&FormData::fields,
ElementsAre(
AllOf(Property(&FormFieldData::name, u"name1"),
Property(&FormFieldData::is_focusable, IsTrue())),
AllOf(Property(&FormFieldData::name, u"name2"),
Property(&FormFieldData::is_focusable, IsFalse()))))));
}
TEST_F(FormAutofillUtilsTest, FindFormByUniqueId) {
LoadHTML("<body><form id='form1'></form><form id='form2'></form></body>");
std::vector<WebFormElement> forms = GetDocument().Forms();
for (const auto& form : forms)
EXPECT_EQ(form, GetFormByRendererId(GetFormRendererId(form)));
// Expect null form element for non-existing form id.
FormRendererId non_existing_form_id(GetFormRendererId(forms[0]).value() +
1000);
EXPECT_FALSE(GetFormByRendererId(non_existing_form_id));
}
// Used in ParameterizedGetFormControlByRendererIdTest.
struct FindFormControlTestParam {
std::string queried_field;
bool expectation;
};
// Tests GetFormControlByRendererId().
class ParameterizedGetFormControlByRendererIdTest
: public FormAutofillUtilsTest,
public testing::WithParamInterface<FindFormControlTestParam> {};
TEST_P(ParameterizedGetFormControlByRendererIdTest,
GetFormControlByRendererId) {
LoadHTML(R"(
<body>
<input id="nonexistentField">
<form id="form1"><input id="ownedField1"></form>
<form id="form2"><input id="ownedField2"></form>
<input id="unownedField">
</body>
)");
WebFormControlElement queried_field =
GetFormControlElementById(GetDocument(), GetParam().queried_field);
FieldRendererId queried_field_id = GetFieldRendererId(queried_field);
ExecuteJavaScriptForTests(
R"(document.getElementById('nonexistentField').remove();)");
content::RunAllTasksUntilIdle();
EXPECT_EQ(GetParam().expectation,
queried_field == GetFormControlByRendererId(queried_field_id));
}
INSTANTIATE_TEST_SUITE_P(
All,
ParameterizedGetFormControlByRendererIdTest,
Values(FindFormControlTestParam{"nonexistentField", false},
FindFormControlTestParam{"ownedField1", true},
FindFormControlTestParam{"ownedField2", true},
FindFormControlTestParam{"unownedField", true}));
// Tests the extraction of the aria-label attribute.
TEST_F(FormAutofillUtilsTest, GetAriaLabel) {
LoadHTML("<input id='input' type='text' aria-label='the label'/>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaLabelForTesting(doc, element), u"the label");
}
// Tests that aria-labelledby works. Simple case: only one id referenced.
TEST_F(FormAutofillUtilsTest, GetAriaLabelledBySingle) {
LoadHTML(
"<div id='billing'>Billing</div>"
"<div>"
" <div id='name'>Name</div>"
" <input id='input' type='text' aria-labelledby='name'/>"
"</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaLabelForTesting(doc, element), u"Name");
}
// Tests that aria-labelledby works: Complex case: multiple ids referenced.
TEST_F(FormAutofillUtilsTest, GetAriaLabelledByMulti) {
LoadHTML(
"<div id='billing'>Billing</div>"
"<div>"
" <div id='name'>Name</div>"
" <input id='input' type='text' aria-labelledby='billing name'/>"
"</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaLabelForTesting(doc, element), u"Billing Name");
}
// Tests that aria-labelledby takes precedence over aria-label
TEST_F(FormAutofillUtilsTest, GetAriaLabelledByTakesPrecedence) {
LoadHTML(
"<div id='billing'>Billing</div>"
"<div>"
" <div id='name'>Name</div>"
" <input id='input' type='text' aria-label='ignored' "
" aria-labelledby='name'/>"
"</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaLabelForTesting(doc, element), u"Name");
}
// Tests that an invalid aria-labelledby reference gets ignored (as opposed to
// crashing, for example).
TEST_F(FormAutofillUtilsTest, GetAriaLabelledByInvalid) {
LoadHTML(
"<div id='billing'>Billing</div>"
"<div>"
" <div id='name'>Name</div>"
" <input id='input' type='text' aria-labelledby='div1 div2'/>"
"</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaLabelForTesting(doc, element), u"");
}
// Tests that invalid aria-labelledby references fall back to aria-label.
TEST_F(FormAutofillUtilsTest, GetAriaLabelledByFallback) {
LoadHTML(
"<div id='billing'>Billing</div>"
"<div>"
" <div id='name'>Name</div>"
" <input id='input' type='text' aria-label='valid' "
" aria-labelledby='div1 div2'/>"
"</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaLabelForTesting(doc, element), u"valid");
}
// Tests that aria-describedby works: Simple case: a single id referenced.
TEST_F(FormAutofillUtilsTest, GetAriaDescriptionBySingle) {
LoadHTML(
"<input id='input' type='text' aria-describedby='div1'/>"
"<div id='div1'>aria description</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaDescriptionForTesting(doc, element), u"aria description");
}
// Tests that aria-describedby works: Complex case: multiple ids referenced.
TEST_F(FormAutofillUtilsTest, GetAriaDescriptionByMulti) {
LoadHTML(
"<input id='input' type='text' aria-describedby='div1 div2'/>"
"<div id='div2'>description</div>"
"<div id='div1'>aria</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaDescriptionForTesting(doc, element), u"aria description");
}
// Tests that invalid aria-describedby returns the empty string.
TEST_F(FormAutofillUtilsTest, GetAriaDescriptionByInvalid) {
LoadHTML("<input id='input' type='text' aria-describedby='invalid'/>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaDescriptionForTesting(doc, element), u"");
}
// Tests that aria-describedby is prioritized over aria-description.
TEST_F(FormAutofillUtilsTest, GetAriaDescriptionPrioritization) {
LoadHTML(
"<input id='input' type='text' aria-describedby='div1'"
" aria-description='aria description'/>"
"<div id='div1'>aria describedby</div>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaDescriptionForTesting(doc, element), u"aria describedby");
}
// Tests that aria-description is used as a fallback if aria-describedby is
// unspecified.
TEST_F(FormAutofillUtilsTest, GetAriaDescriptionFallback) {
LoadHTML(
"<input id='input' type='text' aria-description='aria description'/>");
WebDocument doc = GetDocument();
auto element = GetFormControlElementById(doc, "input");
EXPECT_EQ(GetAriaDescriptionForTesting(doc, element), u"aria description");
}
// Tests IsOwnedByFrame().
TEST_F(FormAutofillUtilsTest, IsOwnedByFrame) {
LoadHTML(R"(
<body>
<div id="div"></div>
<iframe id="child_frame"></iframe>
</body>
)");
WebDocument doc = GetDocument();
content::RenderFrame* main_frame = GetMainRenderFrame();
content::RenderFrame* child_frame = GetIframeById(doc, "child_frame");
WebElement div = GetElementById(doc, "div");
EXPECT_FALSE(IsOwnedByFrame(WebElement(), /*frame=*/nullptr));
EXPECT_FALSE(IsOwnedByFrame(WebElement(), main_frame));
EXPECT_FALSE(IsOwnedByFrame(div, /*frame=*/nullptr));
EXPECT_FALSE(IsOwnedByFrame(div, child_frame));
EXPECT_TRUE(IsOwnedByFrame(div, main_frame));
ExecuteJavaScriptForTests(R"(document.getElementById('div').remove();)");
content::RunAllTasksUntilIdle();
EXPECT_TRUE(IsOwnedByFrame(div, main_frame));
}
TEST_F(FormAutofillUtilsTest, ExtractFormData_IsActionEmptyFalse) {
LoadHTML(
"<body><form id='form1' action='done.html'><input "
"id='i1'></form></body>");
WebDocument doc = GetDocument();
auto web_form = GetFormElementById(doc, "form1");
FormData form_data = *ExtractFormData(web_form);
EXPECT_FALSE(form_data.is_action_empty());
}
TEST_F(FormAutofillUtilsTest, ExtractFormData_IsActionEmptyTrue) {
LoadHTML("<body><form id='form1'><input id='i1'></form></body>");
WebDocument doc = GetDocument();
auto web_form = GetFormElementById(doc, "form1");
FormData form_data = *ExtractFormData(web_form);
EXPECT_TRUE(form_data.is_action_empty());
}
TEST_F(FormAutofillUtilsTest,
FindFormAndFieldForFormControlElement_ExtractBounds) {
LoadHTML("<body><form id='form1'><input id='i1'></form></body>");
WebDocument doc = GetDocument();
auto web_control = GetFormControlElementById(doc, "i1");
std::optional<std::pair<FormData, raw_ref<const FormFieldData>>>
form_and_field = FindFormAndFieldForFormControlElement(web_control);
ASSERT_TRUE(form_and_field);
auto& [form, field] = *form_and_field;
EXPECT_FALSE(form.fields().back().bounds().IsEmpty());
}
TEST_F(FormAutofillUtilsTest,
FindFormAndFieldForFormControlElement_ExtractUnownedBounds) {
LoadHTML("<body><input id='i1'></body>");
WebDocument doc = GetDocument();
auto web_control = GetFormControlElementById(doc, "i1");
std::optional<std::pair<FormData, raw_ref<const FormFieldData>>>
form_and_field = FindFormAndFieldForFormControlElement(web_control);
ASSERT_TRUE(form_and_field);
auto& [form, field] = *form_and_field;
EXPECT_FALSE(form.fields().back().bounds().IsEmpty());
}
TEST_F(FormAutofillUtilsTest,
FindFormAndFieldForFormControlElement_GetDataListOptions) {
LoadHTML(
"<body><input list='datalist_id' name='count' id='i1'><datalist "
"id='datalist_id'><option value='1'><option "
"value='2'></datalist></body>");
WebDocument doc = GetDocument();
auto web_control = GetElementById(doc, "i1").To<WebInputElement>();
std::vector<SelectOption> options = GetDataListOptionsForTesting(web_control);
ASSERT_EQ(options.size(), 2u);
EXPECT_EQ(options[0].value, u"1");
EXPECT_EQ(options[1].value, u"2");
EXPECT_EQ(options[0].text, u"");
EXPECT_EQ(options[1].text, u"");
}
TEST_F(FormAutofillUtilsTest,
FindFormAndFieldForFormControlElement_GetDataListOptionsWithLabels) {
LoadHTML(
"<body><input list='datalist_id' name='count' id='i1'><datalist "
"id='datalist_id'><option value='1'>one</option><option "
"value='2'>two</option></datalist></body>");
WebDocument doc = GetDocument();
auto web_control = GetElementById(doc, "i1").To<WebInputElement>();
std::vector<SelectOption> options = GetDataListOptionsForTesting(web_control);
ASSERT_EQ(options.size(), 2u);
EXPECT_EQ(options[0].value, u"1");
EXPECT_EQ(options[1].value, u"2");
EXPECT_EQ(options[0].text, u"one");
EXPECT_EQ(options[1].text, u"two");
}
TEST_F(FormAutofillUtilsTest,
FindFormAndFieldForFormControlElement_ExtractDataList) {
LoadHTML(
"<body><input list='datalist_id' name='count' id='i1'><datalist "
"id='datalist_id'><option value='1'>one</option><option "
"value='2'>two</option></datalist></body>");
WebDocument doc = GetDocument();
auto web_control = GetElementById(doc, "i1").To<WebInputElement>();
std::optional<std::pair<FormData, raw_ref<const FormFieldData>>>
form_and_field = FindFormAndFieldForFormControlElement(web_control);
ASSERT_TRUE(form_and_field);
auto& [form, field] = *form_and_field;
auto& options = form.fields().back().datalist_options();
ASSERT_EQ(options.size(), 2u);
EXPECT_EQ(options[0].value, u"1");
EXPECT_EQ(options[1].value, u"2");
EXPECT_EQ(options[0].text, u"one");
EXPECT_EQ(options[1].text, u"two");
EXPECT_EQ(field->datalist_options().size(), options.size());
}
TEST_F(FormAutofillUtilsTest,
FindFormAndFieldForFormControlElement_Disconnected) {
LoadHTML(R"(<input name=count id=t>)");
WebDocument doc = GetDocument();
auto form_control = GetElementById(doc, "t").To<WebInputElement>();
ExecuteJavaScriptForTests(R"(document.getElementById('t').remove();)");
EXPECT_EQ(FindFormAndFieldForFormControlElement(form_control), std::nullopt);
}
// Tests that Autofill form ownership follows Blink form's association, which,
// in compliance with the HTML standard, associates forms with an unclosed
// <form> element.
// Regression test for crbug.com/347059988#comment40.
TEST_F(FormAutofillUtilsTest,
FindFormAndFieldForFormControlElement_DramaticallyBadMarkup) {
auto is_ancestor = [](const WebElement& ancestor, WebNode descendant) {
do {
if (ancestor == descendant) {
return true;
}
} while ((descendant = descendant.ParentNode()));
return false;
};
// The following markup is intentionally bad!
LoadHTML(R"(
<!DOCTYPE html>
<div>
<form id=f1>
<div>
</form>
<form id=f2>
</div>
</div>
<input id=t>
)");
// This leads to the same DOM as
// <div>
// <form id=f1>
// <div>
// <form id=f2>
// </form>
// </div>
// </form>
// </div>
// <input id=t>
// but it associates `t` with `f2`.
WebDocument doc = GetDocument();
auto f1 = GetElementById(doc, "f1").To<WebFormElement>();
auto f2 = GetElementById(doc, "f2").To<WebFormElement>();
auto t = GetElementById(doc, "t").To<WebInputElement>();
ASSERT_TRUE(is_ancestor(f1, f2));
ASSERT_FALSE(is_ancestor(f1, t));
ASSERT_EQ(t.Form(), f2); // nocheck
EXPECT_THAT(FindFormAndFieldForFormControlElement(t),
Optional(Pair(AllOf(HasRendererIdOf(f1),
Property(&FormData::fields,
ElementsAre(HasRendererIdOf(t)))),
_)));
}
// Tests the visibility detection of iframes.
// This test checks many scenarios. It's intentionally not a parameterized test
// for performance reasons.
// This test is very similar to the IsWebElementVisibleTest test.
TEST_F(FormAutofillUtilsTest, IsVisibleIframeTest) {
// Test cases of <iframe> elements with different styles.
//
// The `data-[in]visible` attribute represents whether IsVisibleIframe()
// is expected to classify the iframe as [in]visible.
//
// Since IsVisibleIframe() falls short of what the human user will consider
// visible or invisible, there are false positives and false negatives. For
// example, IsVisibleIframe() does not check opacity, so <iframe
// style="opacity: 0.0"> is a false positive (it's visible to
// IsVisibleIframe() but invisible to the human).
//
// The `data-false="{POSITIVE,NEGATIVE}"` attribute indicates whether the test
// case is a false positive/negative compared to human visibility perception.
// In such a case, not meeting the expectation actually indicates an
// improvement of IsVisibleIframe(), as it means a false positive/negative has
// been fixed.
//
// The sole purpose of the `data-false` attribute is to document this and to
// print a message when such a test fails.
LoadHTML(R"(
<body>
<iframe srcdoc="<input>" data-visible style=""></iframe>
<iframe srcdoc="<input>" data-visible style="display: block;"></iframe>
<iframe srcdoc="<input>" data-visible style="visibility: visible;"></iframe>
<iframe srcdoc="<input>" data-invisible style="display: none;"></iframe>
<iframe srcdoc="<input>" data-invisible style="visibility: hidden;"></iframe>
<div style="display: none;"> <iframe srcdoc="<input>" data-invisible></iframe></div>
<div style="visibility: hidden;"><iframe srcdoc="<input>" data-invisible></iframe></div>
<iframe srcdoc="<input>" data-visible style="width: 15px; height: 15px;"></iframe>
<iframe srcdoc="<input>" data-invisible style="width: 15px; height: 5px;"></iframe>
<iframe srcdoc="<input>" data-invisible style="width: 5px; height: 15px;"></iframe>
<iframe srcdoc="<input>" data-invisible style="width: 5px; height: 5px;"></iframe>
<iframe srcdoc="<input>" data-invisible style="width: 1px; height: 1px;"></iframe>
<iframe srcdoc="<input>" data-invisible style="width: 1px; height: 1px; overflow: visible;" data-false="NEGATIVE"></iframe>
<iframe srcdoc="<input>" data-visible style="opacity: 0.0;" data-false="POSITIVE"></iframe>
<iframe srcdoc="<input>" data-visible style="opacity: 0.0;" data-false="POSITIVE"></iframe>
<iframe srcdoc="<input>" data-visible style="position: absolute; clip: rect(0,0,0,0);" data-false="POSITIVE"></iframe>
<iframe srcdoc="<input>" data-visible style="width: 100px; height: 100px; position: absolute; left: -75px;"></iframe>
<iframe srcdoc="<input>" data-visible style="width: 100px; height: 100px; position: absolute; top: -75px;"></iframe>
<iframe srcdoc="<input>" data-visible style="width: 100px; height: 100px; position: absolute; left: -200px;" data-false="POSITIVE"></iframe>
<iframe srcdoc="<input>" data-visible style="width: 100px; height: 100px; position: absolute; top: -200px;" data-false="POSITIVE"></iframe>
<iframe srcdoc="<input>" data-visible style="width: 100px; height: 100px; position: absolute; right: -200px;" data-false="POSITIVE"></iframe>
<iframe srcdoc="<input>" data-visible style="width: 100px; height: 100px; position: absolute; bottom: -200px;" data-false="POSITIVE"></iframe>
<iframe srcdoc="<input>" data-visible style=""></iframe> <!-- Finish with a visible frame to make sure all <iframe> tags have been closed -->
<div style="width: 10000; height: 10000"></div>
</body>)");
// Ensure that Android runs at default page scale.
web_view_->SetPageScaleFactor(1.0);
std::vector<WebElement> iframes = [this] {
WebDocument doc = GetDocument();
std::vector<WebElement> result;
WebElementCollection iframes = doc.GetElementsByHTMLTagName("iframe");
for (WebElement iframe = iframes.FirstItem(); iframe;
iframe = iframes.NextItem()) {
result.push_back(iframe);
}
return result;
}();
ASSERT_GE(iframes.size(), 23u);
auto RunTestCases = [](const std::vector<WebElement>& iframes) {
for (WebElement iframe : iframes) {
gfx::Rect bounds = iframe.BoundsInWidget();
bool expectation = iframe.HasAttribute("data-visible");
SCOPED_TRACE(
testing::Message()
<< "Iframe with style \n " << iframe.GetAttribute("style").Ascii()
<< "\nwith dimensions w=" << bounds.width()
<< ",h=" << bounds.height() << " and position x=" << bounds.x()
<< ",y=" << bounds.y()
<< (iframe.HasAttribute("data-false") ? "\nwhich used to be a FALSE "
: "")
<< iframe.GetAttribute("data-false").Ascii());
ASSERT_TRUE(iframe.HasAttribute("data-visible") !=
iframe.HasAttribute("data-invisible"));
EXPECT_EQ(IsVisibleIframeForTesting(iframe), expectation);
}
};
RunTestCases(iframes);
{
ExecuteJavaScriptForTests(
"window.scrollTo(document.body.scrollWidth,document.body.scrollHeight)"
";");
content::RunAllTasksUntilIdle();
SCOPED_TRACE(testing::Message() << "Scrolled to bottom right");
RunTestCases(iframes);
}
}
// Tests the visibility detection of fields.
// This test checks many scenarios. It's intentionally not a parameterized test
// for performance reasons.
// This test is very similar to the IsVisibleIframeTest test.
TEST_F(FormAutofillUtilsTest, IsWebElementVisibleTest) {
// Test cases of <input> elements with different types and styles.
//
// The `data-[in]visible` attribute represents whether IsWebElementVisible()
// is expected to classify the input as [in]visible.
//
// Since IsWebElementVisible() falls short of what the human user will
// consider visible or invisible, there are false positives and false
// negatives. For example, IsWebElementVisible() does not check opacity, so
// <input style="opacity: 0.0"> is a false positive (it's visible to
// IsWebElementVisible() but invisible to the human).
//
// The `data-false="{POSITIVE,NEGATIVE}"` attribute indicates whether the test
// case is a false positive/negative compared to human visibility perception.
// In such a case, not meeting the expectation actually indicates an
// improvement of IsWebElementVisible(), as it means a false positive/negative
// has been fixed.
//
// The sole purpose of the `data-false` attribute is to document this and to
// print a message when such a test fails.
LoadHTML(R"(
<body>
<input type="text" data-visible style="">
<input type="text" data-visible style="display: block;">
<input type="text" data-visible style="visibility: visible;">
<input type="text" data-invisible style="display: none;">
<input type="text" data-invisible style="visibility: hidden;">
<div style="display: none;"> <input type="text" data-invisible></div>
<div style="visibility: hidden;"><input type="text" data-invisible></div>
<input type="text" data-visible style="width: 15px; height: 15px;">
<input type="text" data-invisible style="width: 15px; height: 5px;">
<input type="text" data-invisible style="width: 5px; height: 15px;">
<input type="text" data-invisible style="width: 5px; height: 5px;">
<input type="text" data-invisible style="width: 1px; height: 1px;">
<input type="text" data-invisible style="width: 1px; height: 1px; overflow: visible;" data-false="NEGATIVE">
<input type="text" data-visible style="opacity: 0.0;" data-false="POSITIVE">
<input type="text" data-visible style="opacity: 0.0;" data-false="POSITIVE">
<input type="text" data-visible style="position: absolute; clip: rect(0,0,0,0);" data-false="POSITIVE">
<input type="text" data-visible style="width: 100px; position: absolute; left: -75px;">
<input type="text" data-visible style="width: 100px; position: absolute; top: -75px;">
<input type="text" data-visible style="width: 100px; position: absolute; left: -200px;" data-false="POSITIVE">
<input type="text" data-visible style="width: 100px; position: absolute; top: -200px;" data-false="POSITIVE">
<input type="text" data-visible style="width: 100px; position: absolute; right: -200px;" data-false="POSITIVE">
<input type="text" data-visible style="width: 100px; position: absolute; bottom: -200px;" data-false="POSITIVE">
<input type="checkbox" data-visible style="">
<input type="checkbox" data-invisible style="display: none;">
<input type="checkbox" data-invisible style="visibility: hidden;">
<input type="checkbox" data-visible style="width: 15px; height: 15px;">
<input type="checkbox" data-visible style="width: 15px; height: 5px;">
<input type="checkbox" data-visible style="width: 5px; height: 15px;">
<input type="checkbox" data-visible style="width: 5px; height: 5px;">
<input type="radio" data-visible style="">
<input type="radio" data-invisible style="display: none;">
<input type="radio" data-invisible style="visibility: hidden;">
<input type="radio" data-visible style="width: 15px; height: 15px;">
<input type="radio" data-visible style="width: 15px; height: 5px;">
<input type="radio" data-visible style="width: 5px; height: 15px;">
<input type="radio" data-visible style="width: 5px; height: 5px;">
<div style="width: 10000; height: 10000"></div>
</body>)");
// Ensure that Android runs at default page scale.
web_view_->SetPageScaleFactor(1.0);
std::vector<WebElement> inputs = [this] {
WebDocument doc = GetDocument();
std::vector<WebElement> result;
WebElementCollection inputs = doc.GetElementsByHTMLTagName("input");
for (WebElement input = inputs.FirstItem(); input;
input = inputs.NextItem()) {
result.push_back(input);
}
return result;
}();
ASSERT_GE(inputs.size(), 36u);
auto RunTestCases = [](const std::vector<WebElement>& inputs) {
for (WebElement input : inputs) {
gfx::Rect bounds = input.BoundsInWidget();
bool expectation = input.HasAttribute("data-visible");
SCOPED_TRACE(
testing::Message()
<< "Iframe with style \n " << input.GetAttribute("style").Ascii()
<< "\nwith dimensions w=" << bounds.width()
<< ",h=" << bounds.height() << " and position x=" << bounds.x()
<< ",y=" << bounds.y()
<< (input.HasAttribute("data-false") ? "\nwhich used to be a FALSE "
: "")
<< input.GetAttribute("data-false").Ascii());
ASSERT_TRUE(input.HasAttribute("data-visible") !=
input.HasAttribute("data-invisible"));
EXPECT_EQ(IsWebElementVisibleForTesting(input), expectation);
}
};
RunTestCases(inputs);
{
ExecuteJavaScriptForTests(
"window.scrollTo(document.body.scrollWidth,document.body.scrollHeight)"
";");
content::RunAllTasksUntilIdle();
SCOPED_TRACE(testing::Message() << "Scrolled to bottom right");
RunTestCases(inputs);
}
}
// Tests `GetClosestAncestorFormElement(element)`.
TEST_F(FormAutofillUtilsTest, GetClosestAncestorFormElement) {
LoadHTML(R"(
<body>
<iframe id=unowned></iframe>
<form id=outer_form>
<iframe id=owned1></iframe>
<!-- A nested 'inner_form' with an iframe 'owned2' will be
created dynamically. -->
<form id=non_existent>
<iframe id=owned3></iframe>
</form>
</form>
</body>)");
ExecuteJavaScriptForTests(R"(
const inner_form = document.createElement('form');
inner_form.id = 'inner_form';
const owned2 = document.createElement('iframe');
owned2.id = 'owned2';
inner_form.appendChild(owned2);
document.getElementById('outer_form').appendChild(inner_form);
)");
content::RunAllTasksUntilIdle();
WebDocument doc = GetDocument();
EXPECT_EQ(
GetClosestAncestorFormElementForTesting(GetElementById(doc, "unowned")),
WebFormElement());
EXPECT_EQ(
GetClosestAncestorFormElementForTesting(GetElementById(doc, "owned1")),
GetFormElementById(doc, "outer_form"));
EXPECT_EQ(
GetClosestAncestorFormElementForTesting(GetElementById(doc, "owned2")),
GetFormElementById(doc, "inner_form"));
EXPECT_EQ(
GetClosestAncestorFormElementForTesting(GetElementById(doc, "owned3")),
GetFormElementById(doc, "outer_form"));
EXPECT_EQ(WebFormControlElement(),
GetFormElementById(doc, "non_existent_form", AllowNull(true)));
}
// Tests that `IsDOMPredecessor(lhs, rhs, ancestor_hint)` holds iff a DOM
// traversal visits the DOM element with ID `lhs` before the one with ID `rhs`,
// where `ancestor_hint` is the ID of an ancestor DOM node.
//
// For this test, DOM element IDs should be named so that if X as visited
// before Y, then X.id is lexicographically less than Y.id.
TEST_F(FormAutofillUtilsTest, IsDomPredecessorTest) {
LoadHTML(R"(
<body id=0>
<div id=00>
<input id=000>
<input id=001>
<div id=002>
<input id=0020>
</div>
<div id=003>
<input id=0030>
</div>
<input id=004>
</div>
<div id=01>
<iframe id=010></iframe>
<input id=011>
</div>
</body>)");
// The parameter type of IsDomPredecessorTest. The attributes are the IDs of
// the left and right hand side DOM nodes that are to be compared, and some
// common ancestor of them.
struct IsDomPredecessorTestParam {
std::string lhs_id;
std::string rhs_id;
std::vector<std::string> ancestor_hint_ids = {"", "0", "00",
"002", "003", "01"};
};
std::vector<IsDomPredecessorTestParam> test_cases = {
IsDomPredecessorTestParam{"000", "000"},
IsDomPredecessorTestParam{"001", "001"},
IsDomPredecessorTestParam{"000", "001"},
IsDomPredecessorTestParam{"000", "001"},
IsDomPredecessorTestParam{"000", "0020"},
IsDomPredecessorTestParam{"000", "0020"},
IsDomPredecessorTestParam{"000", "004"},
IsDomPredecessorTestParam{"000", "004"},
IsDomPredecessorTestParam{"0020", "0030"},
IsDomPredecessorTestParam{"0020", "0030"},
IsDomPredecessorTestParam{"0030", "004"},
IsDomPredecessorTestParam{"000", "010"},
IsDomPredecessorTestParam{"0030", "010"},
IsDomPredecessorTestParam{"0030", "011"},
IsDomPredecessorTestParam{"010", "011"}};
for (const auto& test : test_cases) {
for (const auto& ancestor_hint_id : test.ancestor_hint_ids) {
SCOPED_TRACE(testing::Message()
<< "lhs=" << test.lhs_id << " rhs=" << test.rhs_id
<< " ancestor_hint_id=" << ancestor_hint_id);
ASSERT_NE(test.lhs_id, ancestor_hint_id);
ASSERT_NE(test.rhs_id, ancestor_hint_id);
WebDocument doc = GetDocument();
WebNode lhs = GetElementById(doc, test.lhs_id);
WebNode rhs = GetElementById(doc, test.rhs_id);
WebNode ancestor_hint = ancestor_hint_id.empty()
? WebNode()
: GetElementById(doc, ancestor_hint_id);
EXPECT_EQ(test.lhs_id < test.rhs_id,
IsDOMPredecessorForTesting(lhs, rhs, ancestor_hint));
EXPECT_EQ(test.rhs_id < test.lhs_id,
IsDOMPredecessorForTesting(rhs, lhs, ancestor_hint));
}
}
}
// The DOM ID of an <input> or <iframe>.
struct FieldOrFrame {
bool is_frame = false;
const char* id;
};
// A FieldFramesTest test case contains HTML code. The form with DOM ID
// |form_id| (nullptr for the synthetic form) shall be extracted and its fields
// and frames shall match |fields_and_frames|.
struct FieldFramesTestParam {
std::string html;
const char* form_id;
std::vector<FieldOrFrame> fields_and_frames;
};
class FieldFramesTest
: public FormAutofillUtilsTest,
public testing::WithParamInterface<FieldFramesTestParam> {
public:
FieldFramesTest() = default;
~FieldFramesTest() override = default;
};
// Check if the unowned form control elements are properly extracted.
// Form control elements are button, fieldset, input, textarea, output and
// select elements.
TEST_F(FormAutofillUtilsTest, GetFormFieldElements_Unowned) {
LoadHTML(R"(
<button id='unowned_button'>Unowned button</button>
<fieldset id='unowned_fieldset'>
<label>Unowned fieldset</label>
</fieldset>
<input id='unowned_input'>
<textarea id='unowned_textarea'>I am unowned</textarea>
<output id='unowned_output'>Unowned output</output>
<select id='unowned_select'>
<option value='first'>first</option>
<option value='second' selected>second</option>
</select>
<object id='unowned_object'></object>
<form id='form'>
<button id='form_button'>Form button</button>
<fieldset id='form_fieldset'>
<label>Form fieldset</label>
</fieldset>
<input id='form_input'>
<textarea id='form_textarea'>I am in a form</textarea>
<output id='form_output'>Form output</output>
<select name='form_select' id='form_select'>
<option value='june'>june</option>
<option value='july' selected>july</option>
</select>
<object id='form_object'></object>
</form>
)");
WebDocument doc = GetDocument();
std::vector<WebFormControlElement> unowned_form_fields =
form_util::GetOwnedFormControlsForTesting(doc, WebFormElement());
EXPECT_THAT(unowned_form_fields,
ElementsAre(GetFormControlElementById(doc, "unowned_button"),
GetFormControlElementById(doc, "unowned_fieldset"),
GetFormControlElementById(doc, "unowned_input"),
GetFormControlElementById(doc, "unowned_textarea"),
GetFormControlElementById(doc, "unowned_output"),
GetFormControlElementById(doc, "unowned_select")));
}
// Tests that FormData::fields and FormData::child_frames are extracted fully
// and in the correct relative order.
TEST_P(FieldFramesTest, ExtractFormData_ExtractFieldsAndFrames) {
FieldFramesTestParam test_case = GetParam();
SCOPED_TRACE(testing::Message() << "HTML: " << test_case.html);
LoadHTML(test_case.html.c_str());
WebDocument doc = GetDocument();
// Extract the |form_data|.
auto form_element = test_case.form_id
? GetFormElementById(doc, test_case.form_id)
: WebFormElement();
FormRendererId host_form = GetFormRendererId(form_element);
std::optional<FormData> form_data = ExtractFormData(form_element);
ASSERT_TRUE(form_data);
// Check that all fields and iframes were extracted.
EXPECT_EQ(form_data->fields().size() + form_data->child_frames().size(),
test_case.fields_and_frames.size());
// Check that all fields were extracted. Do so by checking for each |field| in
// `test_case.fields_and_frames` that the DOM element with ID `field.id`
// corresponds to the next (`i`th) field in `form_data->fields`.
size_t i = 0;
for (const FieldOrFrame& field : test_case.fields_and_frames) {
if (field.is_frame)
continue;
SCOPED_TRACE(testing::Message() << "Checking the " << i
<< "th field (id = " << field.id << ")");
WebElement element = GetElementById(doc, field.id);
ASSERT_TRUE(element);
ASSERT_TRUE(element.IsFormControlElement());
EXPECT_EQ(form_data->fields()[i].host_form_id(), host_form);
EXPECT_TRUE(HaveSameFormControlId(element.To<WebFormControlElement>(),
form_data->fields()[i]));
++i;
}
// Check that all frames were extracted into `form_data->child_frames`
// analogously to the above loop for `form_data->fields`.
//
// In addition, check that `form_data->child_frames[i].predecessor` encodes
// the correct ordering, i.e., that `form_data->child_frames[i].predecessor`
// is the index of the last field in `form_data->fields` that precedes the
// `i`th frame in `form_data->child_frames`.
i = 0;
int preceding_field_index = -1;
for (const auto& frame : test_case.fields_and_frames) {
if (!frame.is_frame) {
++preceding_field_index;
continue;
}
SCOPED_TRACE(testing::Message() << "Checking the " << i
<< "th frame (id = " << frame.id << ")");
auto is_empty = [](auto token) { return token.is_empty(); };
EXPECT_FALSE(std::visit(is_empty, form_data->child_frames()[i].token));
EXPECT_EQ(form_data->child_frames()[i].token, GetFrameToken(doc, frame.id));
EXPECT_EQ(form_data->child_frames()[i].predecessor, preceding_field_index);
++i;
}
}
// Creates 32 test cases containing forms which differ in five bits: whether or
// not the form of interest is a synthetic form, and whether the first, second,
// third, and fourth element are a form control field or an iframe. This form is
// additionally surrounded by two other forms before and after itself. An
// example:
// <body>
// <!-- Two forms not of interest follow -->
// <form><input><iframe></iframe></form>
// <input><iframe></iframe>
// <!-- The form of interest follows -->
// <form id='MY_FORM_ID'>
// <input>
// <input>
// <iframe></iframe>
// <iframe></iframe>
// </form>
// <!-- Two forms not of interest follow -->
// <form><input><iframe></iframe></form>
// <input><iframe></iframe>
// </body>
INSTANTIATE_TEST_SUITE_P(
FormAutofillUtilsTest,
FieldFramesTest,
testing::ValuesIn([] {
// Creates a FieldFramesTestParam. The fifth bit encodes whether the form
// is a synthetic form or not, and the first four bits encode whether its
// four elements are fields or frames.
//
// The choice of four is to cover multiple elements of the same kind
// following each other and being surrounded by other fields, e.g.,
// <input><iframe><iframe><input>.
auto MakeTestCase = [](std::bitset<5> bitset) {
std::vector<FieldOrFrame> fields_and_frames{
{.is_frame = bitset.test(0), .id = "0"},
{.is_frame = bitset.test(1), .id = "1"},
{.is_frame = bitset.test(2), .id = "2"},
{.is_frame = bitset.test(3), .id = "3"},
};
bool is_synthetic_form = bitset.test(4);
const char* form_id = is_synthetic_form ? nullptr : "MY_FORM_ID";
// Create a HTML page according to |is_synthetic_form| and
// |fields_and_frames|: it contains four <input> or <iframe> elements,
// potentially contained in a <form>. Additionally, before and after
// this form, it contains some other <input> and <iframe> elements that
// do not belong to the form of interest.
std::string html;
for (const FieldOrFrame& field_or_frame : fields_and_frames) {
html +=
field_or_frame.is_frame
? base::StringPrintf("<iframe id='%s'></iframe>",
field_or_frame.id)
: base::StringPrintf("<input id='%s'>", field_or_frame.id);
}
if (!is_synthetic_form) {
html = base::StringPrintf("<form id='%s'>%s</form>", form_id,
html.c_str());
const char* other_forms =
"<input><iframe></iframe> <form><input><iframe></iframe></form>";
html = base::StrCat({other_forms, html, other_forms});
} else {
const char* other_form = "<form><input><iframe></iframe></form>";
html = base::StrCat({other_form, html, other_form});
}
html = base::StringPrintf("<body>%s</body>", html.c_str());
return FieldFramesTestParam{.html = html,
.form_id = form_id,
.fields_and_frames = fields_and_frames};
};
// Create all combinations of test cases.
std::vector<FieldFramesTestParam> cases;
for (uint64_t bitset = 0; bitset < (1 << 5); ++bitset)
cases.push_back(MakeTestCase(std::bitset<5>(bitset)));
// Check that we have 32 distinct test cases.
DCHECK_EQ(cases.size(), 32u);
DCHECK(std::ranges::all_of(cases, [&](const auto& case1) {
return std::ranges::all_of(cases, [&](const auto& case2) {
return &case1 == &case2 || case1.html != case2.html;
});
}));
return cases;
}()));
TEST_F(FormAutofillUtilsTest, ExtractFormData_WebFormElementToFormData) {
LoadHTML(R"(
<form id='form'>
<input id='input'>
<select name='form_select' id='select'>
<option value='june'>june</option>
<option value='july' selected>july</option>
</select>
</form>
)");
WebDocument doc = GetDocument();
auto form_element = GetFormElementById(doc, "form");
FormData form_data = *ExtractFormData(form_element);
EXPECT_EQ(form_data.fields().size(), 2u);
{
WebElement element = GetElementById(doc, "input");
ASSERT_TRUE(element);
ASSERT_TRUE(element.IsFormControlElement());
EXPECT_TRUE(HaveSameFormControlId(element.To<WebFormControlElement>(),
form_data.fields()[0]));
}
WebElement element = GetElementById(doc, "select");
ASSERT_TRUE(element);
ASSERT_TRUE(element.IsFormControlElement());
EXPECT_TRUE(HaveSameFormControlId(element.To<WebFormControlElement>(),
form_data.fields()[1]));
}
// Tests that if the number of iframes exceeds kMaxExtractableChildFrames,
// child frames of that form are not extracted.
TEST_F(FormAutofillUtilsTest, ExtractFormData_ExtractNoFramesIfTooManyIframes) {
auto AddElementToForm = [this](const char* element) {
std::string js = base::StringPrintf(
"document.forms[0].appendChild(document.createElement('%s'))", element);
ExecuteJavaScriptForTests(js.c_str());
};
LoadHTML(R"(<html><body><form id='f'></form>)");
for (size_t i = 0; i < kMaxExtractableFields; ++i) {
AddElementToForm("input");
}
for (size_t i = 0; i < kMaxExtractableChildFrames; ++i) {
AddElementToForm("iframe");
}
// Ensure that Android runs at default page scale.
web_view_->SetPageScaleFactor(1.0);
WebDocument doc = GetDocument();
WebFormElement form_element = GetFormElementById(doc, "f");
FormData form_data = *ExtractFormData(form_element);
EXPECT_EQ(form_data.fields().size(), kMaxExtractableFields);
EXPECT_EQ(form_data.child_frames().size(), kMaxExtractableChildFrames);
// Upon adding one more frame, this exceeds the limit and therefore we start
// returning a form with no iframes.
AddElementToForm("iframe");
form_data = *ExtractFormData(form_element);
EXPECT_EQ(form_data.fields().size(), kMaxExtractableFields);
EXPECT_TRUE(form_data.child_frames().empty());
}
// Tests that if the number of fields exceeds |kMaxExtractableFields|, neither
// fields nor child frames of that form are extracted.
TEST_F(FormAutofillUtilsTest, ExtractNoFieldsOrFramesIfTooManyFields) {
auto AddElementToForm = [this](const char* element) {
std::string js = base::StringPrintf(
"document.forms[0].appendChild(document.createElement('%s'))", element);
ExecuteJavaScriptForTests(js.c_str());
};
LoadHTML(R"(<html><body><form id='f'></form>)");
for (size_t i = 0; i < kMaxExtractableFields; ++i) {
AddElementToForm("input");
}
for (size_t i = 0; i < kMaxExtractableChildFrames; ++i) {
AddElementToForm("iframe");
}
// Ensure that Android runs at default page scale.
web_view_->SetPageScaleFactor(1.0);
WebDocument doc = GetDocument();
WebFormElement form_element = GetFormElementById(doc, "f");
FormData form_data = *ExtractFormData(form_element);
EXPECT_EQ(form_data.fields().size(), kMaxExtractableFields);
EXPECT_EQ(form_data.child_frames().size(), kMaxExtractableChildFrames);
// Upon adding one more field, this exceeds the limit and therefore we start
// returning a null form.
AddElementToForm("input");
ASSERT_FALSE(ExtractFormData(form_element));
}
// Verifies that the callback happens even if no sequences of 4 digits are
// found.
TEST_F(FormAutofillUtilsTest, TraverseDomForFourDigitCombinations_NoMatches) {
std::vector<std::string> matches = {"dummy data"};
LoadHTML(R"(123 444)");
WebDocument document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
EXPECT_THAT(matches, IsEmpty());
}
// Verifies that the matches correctly returns all four digit combinations.
TEST_F(FormAutofillUtilsTest,
TraverseDomForFourDigitCombinations_MatchesFound) {
std::vector<std::string> matches;
LoadHTML(R"(
<body>
<p>1234 ****2345 **3456 **** 4567 ●●●●5678 </p>
<form>
<input>
</form>
</body>)");
WebDocument document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
EXPECT_THAT(matches, ElementsAre("1234", "2345", "3456", "4567", "5678"));
LoadHTML(R"(
<form>Enter your CVC for card 2345:
<input type="text">
</form>)");
document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
EXPECT_THAT(matches, ElementsAre("2345"));
LoadHTML(R"(
<table>
<tr>
<td>Enter your CVC for card 2345</td>
<td>
<form><input type="text"></form>
</td>
</tr>
</table>)");
document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
EXPECT_THAT(matches, ElementsAre("2345"));
}
// Ensure that we don't return duplicate values.
TEST_F(FormAutofillUtilsTest,
TraverseDomForFourDigitCombinations_MatchesFoundWithDuplicates) {
std::vector<std::string> matches;
LoadHTML(R"(
<body>
<p>1234 ****1234 **1234 **** 1234 ····1234 ●●●●1234</p>
<form>
<input></input>
</form>
</body>)");
WebDocument document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
// After deduping, we only have one final match.
EXPECT_THAT(matches, ElementsAre("1234"));
}
// Ensures that we correctly perform checks on the last four digit combinations
// for year values.
TEST_F(FormAutofillUtilsTest,
TraverseDomForFourDigitCombinations_YearsRemoved) {
std::vector<std::string> matches = {"dummy_data"};
LoadHTML(R"(
<body>
<form>
<p>1999 2000 1234 2001 2002 2003 2004</p>
</form>
</body>)");
WebDocument document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
// We have no matches as they are years.
EXPECT_THAT(matches, IsEmpty());
LoadHTML(R"(
<body>
<form>
<select>
<option value="1998">1998</option>
<option value="1999">1999</option>
<option value="2000">2000</option>
<option value="2001">2001</option>
<option value="2002">2002</option>
</select>
</form>
</body>)");
document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
// We have no matches as there are more than two years.
EXPECT_THAT(matches, IsEmpty());
LoadHTML(R"(
<body>
<form>
<select>
<option value="1999">1999</option>
<option value="2000">2000</option>
<option value="4545">4545</option>
<option value="6782">6782</option>
</select>
</form>
</body>)");
document = GetDocument();
TraverseDomForFourDigitCombinations(
document, base::BindLambdaForTesting(
[&](const std::vector<std::string>& regex_search) {
matches = regex_search;
}));
// We keep all four matches as there are potential years but not enough to
// disqualify.
EXPECT_THAT(matches, ElementsAre("1999", "2000", "4545", "6782"));
}
MATCHER(SameNode, "") {
return std::get<0>(arg).Equals(std::get<1>(arg));
}
void PrefixTraverseAndAppend(WebNode node, std::vector<WebNode>& out) {
out.push_back(node);
for (WebNode child = node.FirstChild(); child; child = child.NextSibling()) {
PrefixTraverseAndAppend(child, out);
}
}
// Tests that the appropriate web node is returned when iterating through the
// web DOM in forward direction.
TEST_F(FormAutofillUtilsTest, NextWebNode_Forward) {
LoadHTML(R"(
<html>
<head></head>
<body>
<div>
<div>
<div>A</div>
<div>B</div>
</div>
<div>
<div>C</div>
<div>D</div>
<div>E</div>
</div>
<div>
<div>F</div>
<div>G</div>
</div>
</div>
</body>
</html>)");
std::vector<WebNode> expected_elements;
PrefixTraverseAndAppend(GetDocument(), expected_elements);
std::vector<WebNode> found_elements;
for (WebNode node = GetDocument(); node;
node = NextWebNodeForTesting(node, /*forward=*/true)) {
found_elements.push_back(node);
}
EXPECT_THAT(found_elements, Pointwise(SameNode(), expected_elements));
}
// Tests that the appropriate web node is returned when iterating through the
// web DOM in backwards direction.
TEST_F(FormAutofillUtilsTest, NextWebNode_Backward) {
LoadHTML(R"(
<html>
<head></head>
<body>
<div>
<div>
<div>A</div>
<div>B</div>
</div>
<div>
<div>C</div>
<div>D</div>
<div>E</div>
</div>
<div>
<div>F</div>
<div>G</div>
</div>
</div>
</body>
</html>)");
std::vector<WebNode> expected_elements;
PrefixTraverseAndAppend(GetDocument(), expected_elements);
std::ranges::reverse(expected_elements);
std::vector<WebNode> found_elements;
for (WebNode node = expected_elements[0]; node;
node = NextWebNodeForTesting(node, /*forward=*/false)) {
found_elements.push_back(node);
}
EXPECT_THAT(found_elements, Pointwise(SameNode(), expected_elements));
}
// Tests that GetMaxLength() of non-text form controls is 0, and text form
// controls default to the maximum 32 bit integer (and *not* 64 bit integer, so
// that we can still do arithmetic with the maximum length).
TEST_F(FormAutofillUtilsTest, GetMaxLength) {
struct TestCase {
const char* html;
uint64_t expected_max_length;
};
static constexpr TestCase test_cases[] = {
{"<input id=field>", FormFieldData::kDefaultMaxLength},
{"<input id=field type=text>", FormFieldData::kDefaultMaxLength},
{"<input id=field type=text maxlength=-1>",
FormFieldData::kDefaultMaxLength},
{"<input id=field type=password>", FormFieldData::kDefaultMaxLength},
{"<input id=field type=text maxlength=123>", 123},
{"<textarea id=field>", FormFieldData::kDefaultMaxLength},
{"<textarea id=field maxlength=123>", 123},
{"<input id=field type=submit>", 0},
{"<select id=field></select>", 0},
};
for (auto test_case : test_cases) {
SCOPED_TRACE(test_case.html);
LoadHTML(test_case.html);
WebFormControlElement field = GetElementById(GetDocument(), "field")
.DynamicTo<WebFormControlElement>();
EXPECT_TRUE(field);
EXPECT_EQ(test_case.expected_max_length, GetMaxLengthForTesting(field));
}
}
TEST_F(FormAutofillUtilsTest, ContentEditableWritingSuggestionsFalseInherited) {
LoadHTML(
R"(<body writingsuggestions=false>
<div id=my-id contenteditable></div>
</body>)");
WebElement content_editable = GetDocument().GetElementById("my-id");
ASSERT_TRUE(content_editable);
std::optional<FormData> form = FindFormForContentEditable(content_editable);
ASSERT_EQ(form->fields().size(), 1u);
const FormFieldData& field = form->fields()[0];
EXPECT_FALSE(field.allows_writing_suggestions());
}
TEST_F(FormAutofillUtilsTest, ContentEditableWritingSuggestionsFalse) {
LoadHTML(
R"(<body>
<div id=my-id writingsuggestions=false contenteditable></div>
</body>)");
WebElement content_editable = GetDocument().GetElementById("my-id");
ASSERT_TRUE(content_editable);
std::optional<FormData> form = FindFormForContentEditable(content_editable);
ASSERT_EQ(form->fields().size(), 1u);
const FormFieldData& field = form->fields()[0];
EXPECT_FALSE(field.allows_writing_suggestions());
}
TEST_F(FormAutofillUtilsTest, FindFormForContentEditableSuccess) {
LoadHTML(
R"(<body>
<div id=my-id
name=my-name
class=my-class
autocomplete=given-name
contenteditable>
This is the <code>textContent</code>!
</div>
</body>)");
WebElement content_editable = GetDocument().GetElementById("my-id");
ASSERT_TRUE(content_editable);
std::optional<FormData> form = FindFormForContentEditable(content_editable);
ASSERT_EQ(form->fields().size(), 1u);
const FormFieldData& field = form->fields()[0];
EXPECT_TRUE(form->renderer_id());
EXPECT_EQ(*form->renderer_id(), *field.renderer_id());
EXPECT_EQ(form->renderer_id(), field.host_form_id());
EXPECT_EQ(field.parsed_autocomplete()->field_type, HtmlFieldType::kGivenName);
EXPECT_EQ(field.name(), u"my-id");
EXPECT_EQ(field.id_attribute(), u"my-id");
EXPECT_EQ(field.name_attribute(), u"my-name");
EXPECT_EQ(field.css_classes(), u"my-class");
EXPECT_EQ(field.value(),
u"\n This is the textContent!\n ");
EXPECT_TRUE(field.allows_writing_suggestions());
}
TEST_F(FormAutofillUtilsTest, FindFormForContentEditableAbridgedSuccess) {
// HTML with 1500 characters of pi in the contenteditable div
LoadHTML(
R"(<body>
<div id=my-id
name=my-name
class=my-class
autocomplete=given-name
contenteditable>3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029</div>
</body>)");
WebElement content_editable = GetDocument().GetElementById("my-id");
ASSERT_TRUE(content_editable);
std::optional<FormData> form = FindFormForContentEditable(content_editable);
ASSERT_EQ(form->fields().size(), 1u);
const FormFieldData& field = form->fields()[0];
EXPECT_TRUE(form->renderer_id());
EXPECT_EQ(*form->renderer_id(), *field.renderer_id());
EXPECT_EQ(form->renderer_id(), field.host_form_id());
EXPECT_EQ(field.parsed_autocomplete()->field_type, HtmlFieldType::kGivenName);
EXPECT_EQ(field.name(), u"my-id");
EXPECT_EQ(field.id_attribute(), u"my-id");
EXPECT_EQ(field.name_attribute(), u"my-name");
EXPECT_EQ(field.css_classes(), u"my-class");
// Only extract 1024 characters from the div.
EXPECT_EQ(field.value().length(), 1024u);
EXPECT_EQ(
field.value(),
u"3."
u"14159265358979323846264338327950288419716939937510582097494459230781640"
u"62862089986280348253421170679821480865132823066470938446095505822317253"
u"59408128481117450284102701938521105559644622948954930381964428810975665"
u"93344612847564823378678316527120190914564856692346034861045432664821339"
u"36072602491412737245870066063155881748815209209628292540917153643678925"
u"90360011330530548820466521384146951941511609433057270365759591953092186"
u"11738193261179310511854807446237996274956735188575272489122793818301194"
u"91298336733624406566430860213949463952247371907021798609437027705392171"
u"76293176752384674818467669405132000568127145263560827785771342757789609"
u"17363717872146844090122495343014654958537105079227968925892354201995611"
u"21290219608640344181598136297747713099605187072113499999983729780499510"
u"59731732816096318595024459455346908302642522308253344685035261931188171"
u"01000313783875288658753320838142061717766914730359825349042875546873115"
u"95628638823537875937519577818577805321712268066130019278766111959092164"
u"2019893809525720106548586327");
}
TEST_F(FormAutofillUtilsTest, FindFormForContentEditableFailures) {
LoadHTML(
R"(<body>
<div id=ce1></div>
<div contenteditable><div id=ce2 contenteditable></div></div>
<form id=ce3 contenteditable></form>
<textarea id=ce4 contenteditable><div contenteditable></textarea>
</body>)");
WebDocument doc = GetDocument();
ASSERT_FALSE(FindFormForContentEditable(doc.GetElementById("ce1")));
ASSERT_FALSE(FindFormForContentEditable(doc.GetElementById("ce2")));
ASSERT_FALSE(FindFormForContentEditable(doc.GetElementById("ce3")));
ASSERT_FALSE(FindFormForContentEditable(doc.GetElementById("ce4")));
}
TEST_F(FormAutofillUtilsTest, ExtractFormData_OwnedForm) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<html><title>Checkout</title></head>
<form id=form_of_interest>
<input type=text name=text_input>
<input type=checkbox name=check_input>
<input type=number name=number_input>
<select name=select_input>
<option value=option_1></option>
<option value=option_2></option>
</select>
</form>
<form><input type=text name=excluded/></form>
</html>)");
WebDocument doc = GetDocument();
EXPECT_THAT(
ExtractFormData(GetFormElementById(doc, "form_of_interest")),
Optional(Property(
&FormData::fields,
ElementsAre(Property(&FormFieldData::name, u"text_input"),
Property(&FormFieldData::name, u"number_input"),
Property(&FormFieldData::name, u"select_input")))));
histogram_tester.ExpectTotalCount("Autofill.ExtractFormUnowned.FieldCount2",
0);
histogram_tester.ExpectUniqueSample("Autofill.ExtractFormOwned.FieldCount2",
3, 1);
}
TEST_F(FormAutofillUtilsTest, ExtractFormData_UnownedForm) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<html><title>Checkout</title></head>
<input type=text name=text_input>
<input type=checkbox name=check_input>
<input type=number name=number_input>
<select name=select_input>
<option value=option_1></option>
<option value=option_2></option>
</select>
<form><input type=text name=excluded/></form>
</html>)");
WebDocument doc = GetDocument();
EXPECT_THAT(
ExtractFormData(WebFormElement()),
Optional(Property(
&FormData::fields,
ElementsAre(Property(&FormFieldData::name, u"text_input"),
Property(&FormFieldData::name, u"number_input"),
Property(&FormFieldData::name, u"select_input")))));
histogram_tester.ExpectTotalCount("Autofill.ExtractFormOwned.FieldCount2", 0);
histogram_tester.ExpectUniqueSample("Autofill.ExtractFormUnowned.FieldCount2",
3, 1);
}
// Tests that GetOwnedFormControls() doesn't return disconnected elements.
TEST_F(FormAutofillUtilsTest, GetOwnedFormControlsRequiresConnectedness) {
LoadHTML(R"(
<html>
<body>
<form id=f>
<input id=t>
</form>
</body>
</html>)");
WebDocument doc = GetDocument();
WebFormElement f = GetFormElementById(doc, "f");
WebFormControlElement t = GetFormControlElementById(doc, "t");
EXPECT_THAT(f.GetFormControlElements(), ElementsAre(t)); // nocheck
EXPECT_THAT(GetOwnedFormControlsForTesting(doc, f), ElementsAre(t));
ExecuteJavaScriptForTests(R"(
document.getElementById('f').remove();
)");
// Blink still gives us the disconnected element, but in Autofill we don't
// want it.
EXPECT_THAT(f.GetFormControlElements(), ElementsAre(t)); // nocheck
EXPECT_THAT(GetOwnedFormControlsForTesting(doc, f), IsEmpty());
}
// Tests that final-checkout-amount extraction extracts the
// final-checkout-amount if the label node is in the subtree that is only one
// ancestor up.
TEST_F(FormAutofillUtilsTest, ExtractFinalCheckoutAmountFromDom_OneAncestorUp) {
std::vector<std::string> matches;
LoadHTML(R"(
<body>
<div>
<span>Total</span>
<div>$448.60</div>
</div>
</body>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^.448.60$";
std::string_view label_regex = "^Total$";
EXPECT_EQ(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6),
"$448.60");
}
// Tests that final-checkout-amount extraction extracts the
// final-checkout-amount if the label node is in the subtree that is many
// ancestors up.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_ManyAncestorsUp) {
LoadHTML(R"(
<div>
<div>
<div>Total</div>
<div>
<div>
<span>
<span>
<span>$56.70</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^.56.70$";
std::string_view label_regex = "^Total$";
EXPECT_EQ(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6),
"$56.70");
}
// Tests that final-checkout-amount extraction extracts the
// final-checkout-amount if the label node is in the subtree that is many
// ancestors down.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_ManyAncestorsDown) {
LoadHTML(R"(
<div>
<div>
<div>$56.70</div>
<div>
<div>
<span>
<span>
<span>
<div>Total</div>
</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^.56.70$";
std::string_view label_regex = "^Total$";
EXPECT_EQ(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/2),
"$56.70");
}
// Tests that final-checkout-amount extraction does not extract a
// final-checkout-amount if the label node is more than
// `number_of_ancestor_levels_to_search` up from the final-checkout-amount node.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_TooManyAncestorsUp_DoesNotMatch) {
LoadHTML(R"(
<div>
<div>
<div>Total</div>
<div>
<div>
<span>
<span>
<span>$56.70</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^.56.70$";
std::string_view label_regex = "^Total$";
EXPECT_TRUE(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/3)
.empty());
}
// Tests that final-checkout-amount extraction returns the first
// final-checkout-amount match if there are multiple possible matches.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_MultiplePriceNodes_MatchesFirstOne) {
LoadHTML(R"(
<div>
<div>
<div>
<div>
<span>
<span>
<span>$56.70</span>
<span>Total</span>
</span>
<span>
<span>$56.71</span>
<span>Total</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^(.56.70|.56.71)$";
std::string_view label_regex = "^Total$";
std::string final_checkout_amount = ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6);
EXPECT_TRUE(final_checkout_amount == "$56.70" ||
final_checkout_amount == "$56.71");
}
// Tests that final-checkout-amount extraction returns the closest final
// checkout amount match if there are multiple possible matches. The closest
// match is based on the lowest common ancestor between price node and label
// node.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_MultiplePriceNodes_MatchesClosestOne) {
LoadHTML(R"(
<div>
<div>
<div>
<div>
<span>
<span>
<span>
<span>$56.70</span>
</span>
<span>Total</span>
</span>
<span>
<span>$56.71</span>
<span>Total</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^(.56.70|.56.71)$";
std::string_view label_regex = "^Total$";
EXPECT_EQ(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6),
"$56.71");
}
// Tests that final-checkout-amount extraction does not extract a
// final-checkout-amount if there are price nodes in the ancestor searches
// containing the label node.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_MultiplePriceNodes_DoesNotMatch) {
LoadHTML(R"(
<div>
<div>
<div>Total</div>
<div>
<div>
<span>
<span>
<span>$56.70</span>
</span>
<span>
<span>$56.71</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^(.56.70|.56.71)$";
std::string_view label_regex = "^Total$";
EXPECT_TRUE(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6)
.empty());
}
// Tests that final-checkout-amount extraction does not extract a
// final-checkout-amount if the ancestor search of one price node contains
// multiple price nodes, and the ancestor search of the other one does not
// contain the label node.
TEST_F(
FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_MultiplePriceNodesInAncestorSearchOfOne_DoesNotMatch) {
LoadHTML(R"(
<div>
<div>
<span>$56.71</span>
<span>
<div>Total</div>
<span>
<div>
<div>
<span>$56.70</span>
</div>
</div>
</span>
</span>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^(.56.70|.56.71)$";
std::string_view label_regex = "^Total$";
EXPECT_TRUE(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/3)
.empty());
}
// Tests that final-checkout-amount extraction matches a final-checkout-amount
// if the ancestor search of one price node contains multiple price nodes, and
// the ancestor search of the other one contains the label node and only one
// price node.
TEST_F(
FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_MultiplePriceNodesInAncestorSearchOfOne_OtherAncestorPathOnlyHasOne_Matches) {
LoadHTML(R"(
<div>
<div>
<span>$56.71</span>
<div>
<div>
<span>
<div>Total</div>
<span>
<span>$56.70</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^(.56.70|.56.71)$";
std::string_view label_regex = "^Total$";
EXPECT_EQ(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6),
"$56.70");
}
// Tests that the final-checkout-amount extraction does not extract a final
// checkout amount if there is no label node.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_NoLabelNode_DoesNotMatch) {
LoadHTML(R"(
<div>
<div>Not a label</div>
<span>$56.70</span>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^.56.70$";
std::string_view label_regex = "^Total$";
EXPECT_TRUE(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6)
.empty());
}
// Tests that final-checkout-amount extraction does not extract a
// final-checkout-amount if there are no price nodes.
TEST_F(FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_NoPriceNodes_DoesNotMatch) {
LoadHTML(R"(
<div>
<div>Total</div>
<div>Not a final-checkout-amount</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^.56.70$";
std::string_view label_regex = "^Total$";
EXPECT_TRUE(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6)
.empty());
}
// Tests that final-checkout-amount extraction does not extract a
// final-checkout-amount if there are no label nodes and no price nodes.
TEST_F(
FormAutofillUtilsTest,
ExtractFinalCheckoutAmountFromDom_NoPriceNodesAndNoLabelNodes_DoesNotMatch) {
LoadHTML(R"(
<div>
<div>Not a total node</div>
<div>Not a final-checkout-amount</div>
</div>)");
WebDocument document = GetDocument();
std::string_view price_regex = "^.56.70$";
std::string_view label_regex = "^Total$";
EXPECT_TRUE(ExtractFinalCheckoutAmountFromDom(
document, price_regex, label_regex,
/*number_of_ancestor_levels_to_search=*/6)
.empty());
}
// Fixture for testing that forms can[not] be extracted on certain URLs.
class FormAutofillUtilsTest_AdmissibleUrls
: public FormAutofillUtilsTest,
public testing::WithParamInterface<std::pair<std::string_view, bool>> {
public:
std::string_view Url() const { return GetParam().first; }
bool extractable() const { return GetParam().second; }
};
INSTANTIATE_TEST_SUITE_P(
,
FormAutofillUtilsTest_AdmissibleUrls,
testing::Values(std::pair("https://foo.com", true),
std::pair("http://foo.com", true),
std::pair("about:srcdoc", true),
std::pair("data:text/html,blabla", true),
std::pair("about:blank", false),
std::pair("chrome:new-tab-page", false),
std::pair("chrome://autofill-internals", false)));
// Tests that <div contenteditable> can be extracted from admissible URLs.
TEST_P(FormAutofillUtilsTest_AdmissibleUrls, ExtractFormData) {
LoadHTMLWithUrlOverride(R"(
"<form id=f><input></form>"
)",
Url());
std::optional<FormData> form =
ExtractFormData(GetFormElementById(GetDocument(), "f"));
if (extractable()) {
EXPECT_NE(form, std::nullopt);
} else {
EXPECT_EQ(form, std::nullopt);
}
}
// Tests that <div contenteditable> can be extracted from admissible URLs.
TEST_P(FormAutofillUtilsTest_AdmissibleUrls, FindFormForContentEditable) {
LoadHTMLWithUrlOverride(R"(
"<div id=ce contenteditable></div>"
)",
Url());
std::optional<FormData> form =
FindFormForContentEditable(GetDocument().GetElementById("ce"));
if (extractable()) {
EXPECT_NE(form, std::nullopt);
} else {
EXPECT_EQ(form, std::nullopt);
}
}
} // namespace
} // namespace autofill::form_util