blob: a5dc1c0e05db07cab0ca2fbf957fa2c70e72a827 [file] [log] [blame]
// Copyright 2021 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/autofill_agent.h"
#include <stdint.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/task/current_thread.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "components/autofill/content/common/mojom/autofill_driver.mojom.h"
#include "components/autofill/content/renderer/autofill_agent_test_api.h"
#include "components/autofill/content/renderer/autofill_renderer_test.h"
#include "components/autofill/content/renderer/form_autofill_util.h"
#include "components/autofill/content/renderer/form_tracker.h"
#include "components/autofill/content/renderer/test_utils.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_data_test_api.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/navigation_simulator.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/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/common/metrics/document_update_reason.h"
#include "third_party/blink/public/web/web_autofill_state.h"
#include "third_party/blink/public/web/web_form_control_element.h"
#include "third_party/blink/public/web/web_frame_widget.h"
#include "third_party/blink/public/web/web_view.h"
namespace autofill {
namespace {
using ::testing::_;
using ::testing::AllOf;
using ::testing::AtMost;
using ::testing::DoAll;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::IsNull;
using ::testing::Matcher;
using ::testing::NiceMock;
using ::testing::Optional;
using ::testing::Property;
using ::testing::SaveArg;
using ::testing::SizeIs;
constexpr CallTimerState kCallTimerStateDummy = {
.call_site = CallTimerState::CallSite::kUpdateFormCache,
.last_autofill_agent_reset = {},
.last_dom_content_loaded = {},
};
class MockAutofillAgent : public AutofillAgent {
public:
using AutofillAgent::AutofillAgent;
MOCK_METHOD(void, DidDispatchDOMContentLoadedEvent, (), (override));
void OverriddenDidDispatchDOMContentLoadedEvent() {
AutofillAgent::DidDispatchDOMContentLoadedEvent();
}
};
class MockFormTracker : public FormTracker {
public:
using FormTracker::FormTracker;
MOCK_METHOD(void,
ElementDisappeared,
(const blink::WebElement& element),
(override));
};
template <typename... Args>
auto FieldsAre(Args&&... matchers) {
return Property("FormData::fields", &FormData::fields,
ElementsAre(std::forward<Args>(matchers)...));
}
// Matches a `FormData` whose `FormData::fields`' `FormFieldData::id_attribute`
// match `id_attributes`.
template <typename... Args>
auto HasFieldsWithIdAttributes(Args&&... id_attributes) {
return FieldsAre(Property("FormFieldData::id_attribute",
&FormFieldData::id_attribute,
std::u16string(id_attributes))...);
}
// Matches a `FormData` with a specific `FormData::renderer_id`.
auto HasFormId(FormRendererId expectation) {
return Property("FormData::renderer_id", &FormData::renderer_id, expectation);
}
// Matches a `FormData` with a specific `FormData::id_attribute`.
auto HasFormIdAttribute(std::u16string id_attribute) {
return Property("FormData::id_attribute", &FormData::id_attribute,
std::move(id_attribute));
}
auto HasButtonTitles(ButtonTitleList titles) {
return Property("FormFieldData::button_titles", &FormData::button_titles,
std::move(titles));
}
auto HasFieldIdAttribute(std::u16string id_attribute) {
return Property("FormFieldData::id_attribute", &FormFieldData::id_attribute,
std::move(id_attribute));
}
auto HasSelectedText(std::u16string selected_text) {
return Property("FormFieldData::selected_text", &FormFieldData::selected_text,
selected_text);
}
auto HasValue(std::u16string value) {
return Property("FormFieldData::value", &FormFieldData::value,
std::move(value));
}
// Matches a FormData with |num| FormData::fields.
auto HasNumFields(size_t num) {
return Property("FormData::fields", &FormData::fields, SizeIs(num));
}
// Matches a FormData with |num| FormData::child_frames.
auto HasNumChildFrames(size_t num) {
return Property("FormData::child_frames", &FormData::child_frames,
SizeIs(num));
}
// Matches a container with a single element which (the element) matches all
// |element_matchers|.
auto HasSingleElementWhich(auto... element_matchers) {
return AllOf(SizeIs(1), ElementsAre(AllOf(element_matchers...)));
}
auto HasType(FormControlType type) {
return Property(&FormFieldData::form_control_type, type);
}
void EnablePlatformAutofillForFrame(content::RenderFrame* render_frame) {
blink::RendererPreferences preferences =
render_frame->GetWebView()->GetRendererPreferences();
preferences.uses_platform_autofill = true;
render_frame->GetWebView()->SetRendererPreferences(preferences);
}
// TODO(crbug.com/41268731): Add many more test cases.
class AutofillAgentTest : public test::AutofillRendererTest {
public:
void SetUp() override {
test::AutofillRendererTest::SetUp();
std::unique_ptr<MockFormTracker> tracker =
std::make_unique<MockFormTracker>(GetMainRenderFrame(),
autofill_agent());
tracker->SetUserGestureRequired(FormTracker::UserGestureRequired(true));
test_api(autofill_agent()).set_form_tracker(std::move(tracker));
}
FormRendererId GetFormRendererIdById(std::string_view id) {
return form_util::GetFormRendererId(GetWebElementById(id));
}
FieldRendererId GetFieldRendererIdById(std::string_view id) {
return form_util::GetFieldRendererId(GetWebElementById(id));
}
size_t num_extracted_forms() {
return std::ranges::count_if(
test_api(autofill_agent()).form_cache().extracted_forms(),
[](const auto& id_and_form) {
const auto& [id, form] = id_and_form;
return form != nullptr;
});
}
void Focus(const char* id) {
ExecuteJavaScriptForTests(base::StringPrintf(R"(
document.getElementById('%s').focus();
)",
id));
task_environment_.FastForwardBy(base::Milliseconds(500));
task_environment_.RunUntilIdle();
}
void Click(std::string_view target) {
SimulatePointClick(
GetWebElementById(target).BoundsInWidget().CenterPoint());
task_environment_.RunUntilIdle();
}
void RightClick(std::string_view target) {
SimulatePointRightClick(
GetWebElementById(target).BoundsInWidget().CenterPoint());
task_environment_.RunUntilIdle();
}
MockFormTracker& form_tracker() {
return static_cast<MockFormTracker&>(
test_api(autofill_agent()).form_tracker());
}
std::vector<FormFieldData::FillData> GetFieldsForFilling(
const std::vector<FormData>& forms) {
std::vector<FormFieldData::FillData> fields;
for (const FormData& form : forms) {
for (const FormFieldData& field : form.fields()) {
fields.emplace_back(field);
}
}
return fields;
}
};
class AutofillAgentTestWithFeatures : public AutofillAgentTest {
public:
AutofillAgentTestWithFeatures() {
scoped_features_.InitWithFeatures(
/*enabled_features=*/
{features::kAutofillReplaceCachedWebElementsByRendererIds},
/*disabled_features=*/{});
}
private:
base::test::ScopedFeatureList scoped_features_;
};
TEST_F(AutofillAgentTestWithFeatures, FormsSeen_Empty) {
EXPECT_CALL(autofill_driver(), FormsSeen).Times(0);
LoadHTML(R"(<body> </body>)");
WaitForFormsSeen();
}
TEST_F(AutofillAgentTestWithFeatures, FormsSeen_NoEmpty) {
EXPECT_CALL(autofill_driver(), FormsSeen).Times(0);
LoadHTML(R"(<body> <form></form> </body>)");
WaitForFormsSeen();
}
TEST_F(AutofillAgentTestWithFeatures, FormsSeen_NewFormUnowned) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormId(FormRendererId(0)),
HasNumFields(1), HasNumChildFrames(0)),
SizeIs(0)));
LoadHTML(R"(<body> <input> </body>)");
WaitForFormsSeen();
}
TEST_F(AutofillAgentTestWithFeatures, FormsSeen_NewForm) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasNumFields(1), HasNumChildFrames(0)),
SizeIs(0)));
LoadHTML(R"(<body> <form><input></form> </body>)");
WaitForFormsSeen();
}
TEST_F(AutofillAgentTestWithFeatures, FormsSeen_NewIframe) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasNumFields(0), HasNumChildFrames(1)),
SizeIs(0)));
LoadHTML(R"(<body> <form><iframe></iframe></form> </body>)");
WaitForFormsSeen();
}
TEST_F(AutofillAgentTestWithFeatures, FormsSeen_UpdatedForm) {
{
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasNumFields(1), HasNumChildFrames(0)),
SizeIs(0)));
LoadHTML(R"(<body> <form><input></form> </body>)");
WaitForFormsSeen();
}
{
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasNumFields(2), HasNumChildFrames(0)),
SizeIs(0)));
ExecuteJavaScriptForTests(
R"(document.forms[0].appendChild(document.createElement('input'));)");
WaitForFormsSeen();
}
}
TEST_F(AutofillAgentTestWithFeatures, TriggerFormExtractionWithResponse) {
EXPECT_CALL(autofill_driver(), FormsSeen);
LoadHTML(R"(<body> <input> </body>)");
WaitForFormsSeen();
base::MockOnceCallback<void(bool)> mock_callback;
EXPECT_CALL(mock_callback, Run).Times(0);
autofill_agent().TriggerFormExtractionWithResponse(mock_callback.Get());
task_environment_.FastForwardBy(AutofillAgent::kFormsSeenThrottle / 2);
EXPECT_CALL(mock_callback, Run(true));
task_environment_.FastForwardBy(AutofillAgent::kFormsSeenThrottle / 2);
}
// Tests that button titles are extracted and reported to the browser.
TEST_F(AutofillAgentTestWithFeatures, ButtonTitlesExtractedForForm) {
ButtonTitleInfo expected_button = {
u"Submit", mojom::ButtonTitleType::INPUT_ELEMENT_SUBMIT_TYPE};
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u"f1"),
HasFieldsWithIdAttributes(u"t1", u"t2"),
HasButtonTitles({expected_button})),
IsEmpty()));
LoadHTML(
R"(<body>
<form id="f1">
<input type="text" id="t1">
<input type="text" id="t2">
<input type="submit" value="Submit" id="b1">
</form>
</body>)");
WaitForFormsSeen();
}
// Tests that button titles are not extracted for fields that are not under a
// <form> tag.
TEST_F(AutofillAgentTestWithFeatures,
ButtonTitlesNotExtractedForFormlessFields) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u""),
HasFieldsWithIdAttributes(u"t1", u"t2"),
HasButtonTitles({})),
IsEmpty()));
LoadHTML(
R"(<body>
<input type="text" id="t1">
<input type="text" id="t2">
<input type="submit" id="b1">
</body>)");
WaitForFormsSeen();
}
using AutofillAgentShadowDomTest = AutofillAgentTestWithFeatures;
// Tests that unassociated form control elements in a Shadow DOM tree that do
// not have a form ancestor are extracted correctly.
TEST_F(AutofillAgentShadowDomTest, UnownedUnassociatedElements) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFieldsWithIdAttributes(u"t1", u"t2")),
IsEmpty()));
LoadHTML(R"(<body>
<div>
<template shadowrootmode="open">
<input type="text" id="t1">
</template>
</div>
<input type="text" id="t2">
</body>)");
WaitForFormsSeen();
}
// Tests that unassociated form control elements whose closest shadow-tree
// including form ancestor is not in a shadow tree are extracted correctly.
TEST_F(AutofillAgentShadowDomTest, UnassociatedElementsOwnedByNonShadowForm) {
EXPECT_CALL(autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u"f1"),
HasFieldsWithIdAttributes(
u"t1", u"t2", u"t3", u"t4")),
IsEmpty()));
LoadHTML(
R"(<body><form id="f1">
<div>
<template shadowrootmode="open">
<input type="text" id="t1">
<input type="text" id="t2">
</template>
</div>
<div>
<template shadowrootmode="open">
<input type="text" id="t3">
</template>
</div>
<input type="text" id="t4">
</form></body>)");
WaitForFormsSeen();
}
// Tests that form control elements that are placed into a slot that is a child
// of a form inside a shadow DOM are not considered to be owned by the form
// inside the shadow DOM, but are considered to be unowned. This is consistent
// with how the DOM handles these form control elements - the "elements" of the
// form "ft" are considered to be empty.
TEST_F(AutofillAgentShadowDomTest, FormControlInsideSlotWithinFormInShadowDom) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u""),
HasFieldsWithIdAttributes(u"t1", u"t2")),
IsEmpty()));
LoadHTML(
R"(<body>
<div>
<template shadowrootmode=open>
<form id=ft>
<slot></slot>
</form>
</template>
<input id=t1>
<input id=t2>
</div>
</body>)");
WaitForFormsSeen();
}
// Tests that a form that is inside a shadow tree and does not have a
// shadow-tree-including form ancestor is extracted correctly.
TEST_F(AutofillAgentShadowDomTest, ElementsOwnedByFormInShadowTree) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u"f1"),
HasFieldsWithIdAttributes(u"t1", u"t2")),
IsEmpty()));
LoadHTML(R"(<body>
<div>
<template shadowrootmode="open">
<form id="f1">
<input type="text" id="t1">
<input type="text" id="t2">
</form>
</template>
</div></body>)");
WaitForFormsSeen();
}
// Tests that a form whose shadow-tree including descendants include another
// form element, is extracted correctly.
TEST_F(AutofillAgentShadowDomTest, NestedForms) {
EXPECT_CALL(autofill_driver(),
FormsSeen(HasSingleElementWhich(
HasFormIdAttribute(u"f1"),
HasFieldsWithIdAttributes(u"t1", u"t2", u"t3")),
IsEmpty()));
LoadHTML(R"(<body><form id="f1">
<div>
<template shadowrootmode="open">
<form id="f2">
<input type="text" id="t1">
<input type="text" id="t2">
</form>
</template>
<input type="text" id="t3">
</div></form></body>)");
WaitForFormsSeen();
}
// Tests that explicit form associations are handled correctly.
TEST_F(AutofillAgentShadowDomTest, NestedFormsWithAssociation) {
EXPECT_CALL(autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u"f1"),
HasFieldsWithIdAttributes(
u"t1", u"t2", u"t3", u"t4",
u"t5", u"t6", u"t7", u"t8")),
IsEmpty()));
LoadHTML(R"(<body><form id="f1">
<div>
<template shadowrootmode="open">
<form id="f2">
<input id="t1">
<input id="t2">
<input id="t3" form="f3">
</form>
<form id=f3">
<input id="t4">
<input id="t5" form="f2">
</form>
<input id="t6" form="f2">
</template>
<input id="t7">
</div></form>
<input id="t8" form="f1">
</body>)");
WaitForFormsSeen();
}
// Tests that multiple nested shadow DOM forms are extracted properly.
TEST_F(AutofillAgentShadowDomTest, MultipleNestedForms) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u"f1"),
HasFieldsWithIdAttributes(
u"t1", u"t2", u"t3", u"t4", u"t5")),
IsEmpty()));
LoadHTML(R"(<body><form id="f1">
<div>
<template shadowrootmode="open">
<form id="f2">
<input type="text" id="t1">
<input type="text" id="t2">
</form>
</template>
</div>
<input type="text" id="t3">
<div>
<template shadowrootmode="open">
<form id="f3">
<input type="text" id="t4">
<input type="text" id="t5">
</form>
</template>
</div>
</form></body>)");
WaitForFormsSeen();
}
// Tests that nested shadow DOM forms are extracted properly even if the nesting
// is multiple levels deep.
TEST_F(AutofillAgentShadowDomTest, DeepNestedForms) {
EXPECT_CALL(
autofill_driver(),
FormsSeen(HasSingleElementWhich(HasFormIdAttribute(u"f1"),
HasFieldsWithIdAttributes(
u"t1", u"t2", u"t3", u"t4", u"t5")),
IsEmpty()));
LoadHTML(R"(<body><form id="f1">
<div>
<template shadowrootmode="open">
<form id="f2">
<input type="text" id="t1">
<input type="text" id="t2">
<div>
<template shadowrootmode="open">
<input type="text" id="t3">
</template>
</div>
</form>
<div>
<template shadowrootmode="open">
<input type="text" id="t4">
<div>
<template shadowrootmode="open">
<form id="f3">
<input type="text" id="t5">
</form>
</template>
</div>
</template>
</div>
</template>
</div></form></body>)");
WaitForFormsSeen();
}
class AutofillAgentTestExtractLabeledTextNodeValue
: public AutofillAgentTestWithFeatures {
public:
using Callback =
base::MockCallback<base::OnceCallback<void(const std::string&)>>;
};
// This test checks an empty string is bound to the input callback when
// the final checkout amount is not found.
TEST_F(AutofillAgentTestExtractLabeledTextNodeValue,
CallbackIsCalledIfCheckoutAmountIsNotFound) {
LoadHTML(R"(
<body>
<div>
<span>I'm not a total amount keyword</span>
<div>I'm not a total amount</div>
</div>
</body>)");
Callback callback;
EXPECT_CALL(callback, Run(Eq("")));
autofill_agent().ExtractLabeledTextNodeValue(u"^.448.60$", u"^Total$", 4,
callback.Get());
}
// This test checks the correct string representing the final checkout
// amount is bound to the input callback when it is found.
TEST_F(AutofillAgentTestExtractLabeledTextNodeValue,
CallbackIsCalledIfCheckoutAmountIsFound) {
LoadHTML(R"(
<div>
<div>
<div>Total</div>
<div>
<div>
<span>
<span>
<span>$56.70</span>
</span>
</span>
</div>
</div>
</div>
</div>)");
Callback callback;
EXPECT_CALL(callback, Run(Eq("$56.70")));
autofill_agent().ExtractLabeledTextNodeValue(u"^.56.70$", u"^Total$", 6,
callback.Get());
}
// This test checks if latency metrics record as failure case when
// the final checkout amount is not found.
TEST_F(AutofillAgentTestExtractLabeledTextNodeValue,
ExtractLabeledTextNodeValueIsNotFound_Renderer_Latency_Metrics) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<body>
<div>
<span>I'm not a total amount keyword</span>
<div>I'm not a total amount</div>
</div>
</body>)");
Callback callback;
EXPECT_CALL(callback, Run(Eq("")));
autofill_agent().ExtractLabeledTextNodeValue(u"^.448.60$", u"^Total$", 4,
callback.Get());
// Check the failure case records amount extraction latency spent in renderer
// in ms
histogram_tester.ExpectTotalCount(
"Autofill.RendererLabeledAmountExtractionLatency.Success", 0);
histogram_tester.ExpectTotalCount(
"Autofill.RendererLabeledAmountExtractionLatency.Failure", 1);
}
// This test checks if latency metrics record as success case when
// the final checkout amount is found.
TEST_F(AutofillAgentTestExtractLabeledTextNodeValue,
ExtractLabeledTextNodeValueIsFound_Renderer_Latency_Metrics) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<div>
<div>Total: <span>$56.70</span></div>
</div>)");
Callback callback;
EXPECT_CALL(callback, Run(Eq("$56.70")));
autofill_agent().ExtractLabeledTextNodeValue(u"^\\$56\\.70$", u"^Total:", 2,
callback.Get());
// Check the success case records amount extraction latency spent in renderer
// in ms
histogram_tester.ExpectTotalCount(
"Autofill.RendererLabeledAmountExtractionLatency.Success", 1);
histogram_tester.ExpectTotalCount(
"Autofill.RendererLabeledAmountExtractionLatency.Failure", 0);
}
class AutofillAgentTestExtractForms : public AutofillAgentTestWithFeatures {
public:
using Callback = base::MockCallback<
base::OnceCallback<void(const std::optional<FormData>&)>>;
void LoadHTML(const char* html, bool wait_for_forms_seen = true) {
if (wait_for_forms_seen) {
EXPECT_CALL(autofill_driver(), FormsSeen);
}
AutofillAgentTestWithFeatures::LoadHTML(html);
WaitForFormsSeen();
}
};
TEST_F(AutofillAgentTestExtractForms, CallbackIsCalledIfFormIsNotFound) {
LoadHTML("<body>", /*wait_for_forms_seen=*/false);
Callback callback;
EXPECT_CALL(callback, Run(Eq(std::nullopt)));
autofill_agent().ExtractForm(GetFormRendererIdById("f"), callback.Get());
}
TEST_F(AutofillAgentTestExtractForms, CallbackIsCalledForForm) {
const auto is_text_input = HasType(FormControlType::kInputText);
LoadHTML("<body><form id=f><input><input></form>");
Callback callback;
EXPECT_CALL(callback,
Run(Optional(AllOf(
Property(&FormData::renderer_id, GetFormRendererIdById("f")),
Property(&FormData::name, u"f"),
FieldsAre(is_text_input, is_text_input)))));
autofill_agent().ExtractForm(GetFormRendererIdById("f"), callback.Get());
}
TEST_F(AutofillAgentTestExtractForms, CallbackIsCalledForFormlessFields) {
const auto is_text_area = HasType(FormControlType::kTextArea);
LoadHTML(R"(<body><input><input>)");
Callback callback;
EXPECT_CALL(callback, Run(Optional(_)));
autofill_agent().ExtractForm(GetFormRendererIdById("f"), callback.Get());
}
TEST_F(AutofillAgentTestExtractForms, CallbackIsCalledForContentEditable) {
const auto is_content_editable = HasType(FormControlType::kContentEditable);
LoadHTML("<body><div id=ce contenteditable></div>",
/*wait_for_forms_seen=*/false);
base::MockCallback<base::OnceCallback<void(const std::optional<FormData>&)>>
callback;
EXPECT_CALL(callback,
Run(Optional(AllOf(
Property(&FormData::renderer_id, GetFormRendererIdById("ce")),
FieldsAre(is_content_editable)))));
autofill_agent().ExtractForm(GetFormRendererIdById("ce"), callback.Get());
}
TEST_F(AutofillAgentTestWithFeatures,
TriggerFormExtractionWithResponse_CalledTwice) {
EXPECT_CALL(autofill_driver(), FormsSeen);
LoadHTML(R"(<body> <input> </body>)");
WaitForFormsSeen();
base::MockOnceCallback<void(bool)> mock_callback;
autofill_agent().TriggerFormExtractionWithResponse(mock_callback.Get());
EXPECT_CALL(mock_callback, Run(false));
autofill_agent().TriggerFormExtractionWithResponse(mock_callback.Get());
}
// Tests that `AutofillDriver::TriggerSuggestions()` triggers
// `AutofillAgent::AskForValuesToFill()` (which will ultimately trigger
// suggestions).
TEST_F(AutofillAgentTestWithFeatures, TriggerSuggestions) {
EXPECT_CALL(autofill_driver(), FormsSeen);
LoadHTML("<body><input></body>");
WaitForFormsSeen();
EXPECT_CALL(autofill_driver(), AskForValuesToFill);
autofill_agent().TriggerSuggestions(
FieldRendererId(2),
AutofillSuggestionTriggerSource::kFormControlElementClicked);
}
TEST_F(AutofillAgentTestWithFeatures,
TriggerSuggestionsForElementWithDatalist) {
EXPECT_CALL(autofill_driver(), FormsSeen);
LoadHTML(R"(<body><form>
<input id="ff" list="fruits">
<datalist id="fruits">
<option value="Strawberry">
<option value="Apple">
</datalist>
</form></body>)");
WaitForFormsSeen();
FormData form;
EXPECT_CALL(autofill_driver(),
AskForValuesToFill(
Property(&FormData::fields,
ElementsAre(Property(
&FormFieldData::datalist_options,
ElementsAre(SelectOption{.value = u"Strawberry"},
SelectOption{.value = u"Apple"})))),
_, _, _, Eq(std::nullopt)));
autofill_agent().TriggerSuggestions(
GetFieldRendererIdById("ff"),
AutofillSuggestionTriggerSource::kFormControlElementClicked);
task_environment_.RunUntilIdle();
}
// Tests that `AutofillDriver::TriggerSuggestions()` works for contenteditables.
TEST_F(AutofillAgentTestWithFeatures, TriggerSuggestionsForContenteditable) {
LoadHTML("<body><div id=ce contenteditable></div></body>");
FormRendererId form_id = GetFormRendererIdById("ce");
EXPECT_CALL(autofill_driver(), AskForValuesToFill);
autofill_agent().TriggerSuggestions(
FieldRendererId(form_id.value()),
AutofillSuggestionTriggerSource::kComposeDialogLostFocus);
}
// Tests that AutofillAgent::ApplyFormAction(kFill, kPreview) and
// AutofillAgent::ClearPreviewedForm correctly set/reset the autofill state of a
// field.
TEST_F(AutofillAgentTest, PreviewThenClear) {
LoadHTML(R"(
<form id="form_id">
<input id="text_id">
</form>
)");
std::vector<blink::WebFormElement> forms = GetDocument().GetTopLevelForms();
ASSERT_EQ(1U, forms.size());
FormData form = *form_util::ExtractFormData(
forms[0].GetDocument(), forms[0],
*base::MakeRefCounted<FieldDataManager>(), kCallTimerStateDummy,
/*button_titles_cache=*/nullptr);
ASSERT_EQ(form.fields().size(), 1u);
blink::WebFormControlElement field =
GetWebElementById("text_id").DynamicTo<blink::WebFormControlElement>();
ASSERT_TRUE(field);
std::u16string prior_value = form.fields()[0].value();
test_api(form).field(0).set_value(form.fields()[0].value() + u"AUTOFILLED");
test_api(form).field(0).set_is_autofilled(true);
ASSERT_EQ(field.GetAutofillState(), blink::WebAutofillState::kNotFilled);
autofill_agent().ApplyFieldsAction(mojom::FormActionType::kFill,
mojom::ActionPersistence::kPreview,
GetFieldsForFilling({form}));
EXPECT_EQ(field.GetAutofillState(), blink::WebAutofillState::kPreviewed);
autofill_agent().ClearPreviewedForm();
EXPECT_EQ(field.GetAutofillState(), blink::WebAutofillState::kNotFilled);
}
// Tests that when JS adds a non-autofillable element to the DOM, we do not
// trigger a DOM reparse (for performance reasons).
TEST_F(AutofillAgentTest,
DynamicElementNotificationFiltering_AddNonAutofillableElement) {
LoadHTML(R"(<form id="form_id"> <input id="name"></form>)");
const auto& extracted_forms =
test_api(autofill_agent()).form_cache().extracted_forms();
ASSERT_EQ(num_extracted_forms(), 1u);
ASSERT_EQ(extracted_forms.rbegin()->second->fields().size(), 1u);
// Add a button to the form. We also modify the ID attribute of the first
// input to be able to check whether the agent triggered a reparse or not.
ExecuteJavaScriptForTests(R"(
form = document.getElementById('form_id');
button = document.createElement('button');
button.type = submit;
button.id = 'submit_button';
form.appendChild(button);
first_input = form.querySelectorAll('input')[0];
first_input.id = 'new_name'
)");
base::test::RunUntil([&] {
return !test_api(autofill_agent())
.process_forms_after_dynamic_change_timer()
.IsRunning();
});
ASSERT_EQ(num_extracted_forms(), 1u);
ASSERT_EQ(extracted_forms.rbegin()->second->fields().size(), 1u);
// The JS changes to the ID are not reflected in the cache, meaning that the
// cache was not updated as a result of executing the prior JS script.
EXPECT_EQ(extracted_forms.rbegin()->second->fields().front().id_attribute(),
u"name");
}
// Tests that when JS adds an autofillable element to the DOM, we trigger a DOM
// reparse and update the cache.
TEST_F(AutofillAgentTest,
DynamicElementNotificationFiltering_AddAutofillableElement) {
LoadHTML(R"(<form id="form_id"> <input id="name"></form>)");
const auto& extracted_forms =
test_api(autofill_agent()).form_cache().extracted_forms();
ASSERT_EQ(num_extracted_forms(), 1u);
ASSERT_EQ(extracted_forms.rbegin()->second->fields().size(), 1u);
// Add a fourth text field. This should be detected by the agent and should
// trigger a reparse.
ExecuteJavaScriptForTests(R"(
form = document.getElementById('form_id');
second_input = document.createElement('input');
second_input.type = 'text';
second_input.id = 'new_field';
form.appendChild(second_input);
)");
base::test::RunUntil([&] {
return !test_api(autofill_agent())
.process_forms_after_dynamic_change_timer()
.IsRunning();
});
ASSERT_EQ(num_extracted_forms(), 1u);
// The added input should be visible in the cache now.
EXPECT_EQ(extracted_forms.rbegin()->second->fields().size(), 2u);
}
// Tests that when JS adds a new form to the DOM, we trigger a DOM
// reparse and update the cache.
TEST_F(AutofillAgentTest, DynamicElementNotificationFiltering_AddForm) {
LoadHTML(R"(<form id="form_id"> <input id="name"></form>)");
ASSERT_EQ(num_extracted_forms(), 1u);
// Add a second form. This should also be detected by the agent and should
// trigger a reparse.
ExecuteJavaScriptForTests(R"(
second_form = document.createElement('form');
second_form.id = 'second_form';
input = document.createElement('input');
input.type = 'text';
input.id = 'second_form_input';
second_form.appendChild(input);
document.body.appendChild(second_form);
)");
base::test::RunUntil([&] {
return !test_api(autofill_agent())
.process_forms_after_dynamic_change_timer()
.IsRunning();
});
ASSERT_EQ(num_extracted_forms(), 2u);
}
class AutofillAgentSubmissionTest : public AutofillAgentTest,
public testing::WithParamInterface<int> {
public:
AutofillAgentSubmissionTest() {
EXPECT_LE(GetParam(), 5);
std::vector<base::test::FeatureRef> features = {
features::kAutofillUseSubmittedFormInHtmlSubmission,
features::kAutofillPreferSavedFormAsSubmittedForm,
features::kAutofillFixFormTracking,
features::kAutofillReplaceCachedWebElementsByRendererIds,
features::kAutofillReplaceFormElementObserver};
std::vector<base::test::FeatureRef> enabled_features(
features.begin(), features.begin() + GetParam());
std::vector<base::test::FeatureRef> disabled_features(
features.begin() + GetParam(), features.end());
scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
bool prefer_saved_form() {
return base::FeatureList::IsEnabled(
features::kAutofillPreferSavedFormAsSubmittedForm);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(AutofillSubmissionTest,
AutofillAgentSubmissionTest,
::testing::Values(0, 1, 2, 3, 4, 5));
// Test that AutofillAgent::JavaScriptChangedValue updates the
// last interacted saved state.
TEST_P(AutofillAgentSubmissionTest,
JavaScriptChangedValueUpdatesLastInteractedSavedState) {
if (!base::FeatureList::IsEnabled(
features::kAutofillReplaceFormElementObserver)) {
GTEST_SKIP();
}
LoadHTML(R"(<form id="form_id"><input id="text_id"></form>)");
blink::WebFormElement form =
GetWebElementById("form_id").DynamicTo<blink::WebFormElement>();
FormRendererId form_id = form_util::GetFormRendererId(form);
ExecuteJavaScriptForTests(
R"(document.forms[0].elements[0].value = 'js_set_value';)");
std::optional<FormData> provisionally_saved_form =
AutofillAgentTestApi(&autofill_agent()).provisionally_saved_form();
// Since we do not have a tracked form yet, the JS call should not update (in
// this case set) the last interacted form.
ASSERT_FALSE(provisionally_saved_form.has_value());
SimulateUserInputChangeForElementById("text_id", "user_set_value");
provisionally_saved_form =
AutofillAgentTestApi(&autofill_agent()).provisionally_saved_form();
ASSERT_TRUE(provisionally_saved_form.has_value());
EXPECT_EQ(provisionally_saved_form->renderer_id(), form_id);
ASSERT_EQ(1u, provisionally_saved_form->fields().size());
EXPECT_EQ(u"user_set_value", provisionally_saved_form->fields()[0].value());
ExecuteJavaScriptForTests(
R"(document.forms[0].elements[0].value = 'js_set_value';)");
provisionally_saved_form =
AutofillAgentTestApi(&autofill_agent()).provisionally_saved_form();
// Since we now have a tracked form and JS modified the same form, we should
// see the JS modification reflected in the last interacted saved form.
ASSERT_TRUE(provisionally_saved_form.has_value());
EXPECT_EQ(provisionally_saved_form->renderer_id(), form_id);
ASSERT_EQ(1u, provisionally_saved_form->fields().size());
EXPECT_EQ(u"js_set_value", provisionally_saved_form->fields()[0].value());
EXPECT_EQ(u"user_set_value",
provisionally_saved_form->fields()[0].user_input());
}
// Test that AutofillAgent::ApplyFormAction(mojom::ActionPersistence::kFill)
// updates the last interacted saved state when the <input>s have no containing
// <form>.
TEST_P(AutofillAgentSubmissionTest,
FormlessApplyFormActionUpdatesLastInteractedSavedState) {
LoadHTML(R"(
<input id="text_id">
)");
blink::WebFormControlElement field =
GetWebElementById("text_id").DynamicTo<blink::WebFormControlElement>();
ASSERT_TRUE(field);
FormFieldData form_field;
form_util::WebFormControlElementToFormFieldForTesting(
blink::WebFormElement(), field, &autofill_agent().field_data_manager(),
&form_field);
form_field.set_value(u"autofilled");
form_field.set_is_autofilled(true);
ASSERT_EQ(field.GetAutofillState(), blink::WebAutofillState::kNotFilled);
FormData form;
form.set_fields({form_field});
autofill_agent().ApplyFieldsAction(mojom::FormActionType::kFill,
mojom::ActionPersistence::kFill,
GetFieldsForFilling({form}));
ASSERT_EQ(field.GetAutofillState(), blink::WebAutofillState::kAutofilled);
std::optional<FormData> provisionally_saved_form =
AutofillAgentTestApi(&autofill_agent()).provisionally_saved_form();
ASSERT_TRUE(provisionally_saved_form.has_value());
ASSERT_EQ(1u, provisionally_saved_form->fields().size());
EXPECT_EQ(u"autofilled", provisionally_saved_form->fields()[0].value());
}
// Test that AutofillAgent::ApplyFormAction(mojom::ActionPersistence::kFill)
// updates the last interacted saved state when the <input>s have a containing
// <form>.
TEST_P(AutofillAgentSubmissionTest,
FormApplyFormActionUpdatesLastInteractedSavedState) {
LoadHTML(R"(
<form id="form_id">
<input id="text_id">
</form>
)");
blink::WebFormElement form_element =
GetWebElementById("form_id").DynamicTo<blink::WebFormElement>();
ASSERT_EQ(1u, form_element.GetFormControlElements().size());
blink::WebFormControlElement field = form_element.GetFormControlElements()[0];
ASSERT_TRUE(field);
ASSERT_EQ("text_id", field.GetIdAttribute().Ascii());
FormData form = *form_util::ExtractFormData(
form_element.GetDocument(), form_element,
*base::MakeRefCounted<FieldDataManager>(), kCallTimerStateDummy,
/*button_titles_cache=*/nullptr);
ASSERT_EQ(1u, form.fields().size());
test_api(form).field(0).set_value(u"autofilled");
test_api(form).field(0).set_is_autofilled(true);
ASSERT_EQ(field.GetAutofillState(), blink::WebAutofillState::kNotFilled);
autofill_agent().ApplyFieldsAction(mojom::FormActionType::kFill,
mojom::ActionPersistence::kFill,
GetFieldsForFilling({form}));
ASSERT_EQ(field.GetAutofillState(), blink::WebAutofillState::kAutofilled);
std::optional<FormData> provisionally_saved_form =
AutofillAgentTestApi(&autofill_agent()).provisionally_saved_form();
ASSERT_TRUE(provisionally_saved_form.has_value());
ASSERT_EQ(1u, provisionally_saved_form->fields().size());
EXPECT_EQ(u"autofilled", provisionally_saved_form->fields()[0].value());
}
TEST_P(AutofillAgentSubmissionTest,
HideElementTriggersFormTracker_DisplayNone) {
LoadHTML(R"(
<form id="form_id">
<input id="field_id">
</form>
)");
blink::WebElement element = GetWebElementById("field_id");
EXPECT_CALL(form_tracker(), ElementDisappeared(element));
ExecuteJavaScriptForTests(
R"(document.forms[0].elements[0].style.display = 'none';)");
GetWebFrameWidget()->UpdateAllLifecyclePhases(
blink::DocumentUpdateReason::kTest);
}
TEST_P(AutofillAgentSubmissionTest,
HideElementTriggersFormTracker_VisibilityHidden) {
LoadHTML(R"(
<form id="form_id">
<input id="field_id">
</form>
)");
blink::WebElement element = GetWebElementById("field_id");
EXPECT_CALL(form_tracker(), ElementDisappeared(element));
ExecuteJavaScriptForTests(
R"(document.forms[0].elements[0].style.visibility = 'hidden';)");
GetWebFrameWidget()->UpdateAllLifecyclePhases(
blink::DocumentUpdateReason::kTest);
}
TEST_P(AutofillAgentSubmissionTest, HideElementTriggersFormTracker_TypeHidden) {
LoadHTML(R"(
<form id="form_id">
<input id="field_id">
</form>
)");
blink::WebElement element = GetWebElementById("field_id");
EXPECT_CALL(form_tracker(), ElementDisappeared(element));
ExecuteJavaScriptForTests(
R"(document.forms[0].elements[0].setAttribute('type', 'hidden');)");
GetWebFrameWidget()->UpdateAllLifecyclePhases(
blink::DocumentUpdateReason::kTest);
}
TEST_P(AutofillAgentSubmissionTest, HideElementTriggersFormTracker_HiddenTrue) {
LoadHTML(R"(
<form id="form_id">
<input id="field_id">
</form>
)");
blink::WebElement element = GetWebElementById("field_id");
EXPECT_CALL(form_tracker(), ElementDisappeared(element));
ExecuteJavaScriptForTests(
R"(document.forms[0].elements[0].setAttribute('hidden', 'true');)");
GetWebFrameWidget()->UpdateAllLifecyclePhases(
blink::DocumentUpdateReason::kTest);
}
TEST_P(AutofillAgentSubmissionTest, HideElementTriggersFormTracker_ShadowDom) {
LoadHTML(R"(
<form id="form_id">
<div>
<template shadowrootmode="open">
<slot></slot>
</template>
<input id="field_id">
</div>
</form>
)");
blink::WebElement element = GetWebElementById("field_id");
EXPECT_CALL(form_tracker(), ElementDisappeared(element));
ExecuteJavaScriptForTests(R"(field_id.slot = "unknown";)");
GetWebFrameWidget()->UpdateAllLifecyclePhases(
blink::DocumentUpdateReason::kTest);
}
// Test that an inferred form submission as a result of a page deleting ALL of
// the <input>s (that the user has edited) on a page with no <form> sends the
// contents of all of the fields to the browser.
TEST_P(AutofillAgentSubmissionTest,
FormlessOnInferredFormSubmissionAfterXhrAndAllInputsRemoved) {
LoadHTML(R"(
<div id='shipping'>
Name: <input type='text' id='name'><br>
Address: <input type='text' id='address'>
</div>
)");
SimulateUserInputChangeForElementById("name", "Ariel");
SimulateUserInputChangeForElementById("address", "Atlantica");
EXPECT_CALL(autofill_driver(),
FormSubmitted(
AllOf(FieldsAre(HasFieldIdAttribute(u"name"),
HasFieldIdAttribute(u"address")),
FieldsAre(HasValue(u"Ariel"), HasValue(u"Atlantica"))),
_));
// Simulate inferred form submission as a result the focused field being
// removed after an AJAX call.
ExecuteJavaScriptForTests(
R"(document.getElementById('shipping').innerHTML = '')");
autofill_agent().OnFormSubmission(mojom::SubmissionSource::XHR_SUCCEEDED,
/*submitted_form_element=*/std::nullopt);
}
// Tests that an inferred form submission as a result of a page deleting ALL of
// the <input>s that the user has edited but NOT ALL of the <inputs> on the page
// sends the user-edited <inputs> to the browser.
TEST_P(AutofillAgentSubmissionTest,
FormlessOnInferredFormSubmissionAfterXhrAndSomeInputsRemoved) {
LoadHTML(R"(
Search: <input type='text' id='search'><br>
<div id='shipping'>
Name: <input type='text' id='name'><br>
Address: <input type='text' id='address'>
</div>
)");
SimulateUserInputChangeForElementById("name", "Ariel");
SimulateUserInputChangeForElementById("address", "Atlantica");
EXPECT_CALL(autofill_driver(),
FormSubmitted(AllOf(FieldsAre(HasFieldIdAttribute(u"search"),
HasFieldIdAttribute(u"name"),
HasFieldIdAttribute(u"address")),
FieldsAre(HasValue(u""), HasValue(u"Ariel"),
HasValue(u"Atlantica"))),
_));
// Simulate inferred form submission as a result the focused field being
// removed after an AJAX call.
ExecuteJavaScriptForTests(R"(document.getElementById('shipping').remove();)");
autofill_agent().OnFormSubmission(mojom::SubmissionSource::XHR_SUCCEEDED,
/*submitted_form_element=*/std::nullopt);
}
// Test scenario WHERE:
// - AutofillAgent::OnProbablyFormSubmitted() is called as a result of a page
// navigation. AND
// - There is no <form> element.
// AND
// - An <input> other than the last interacted <input> is hidden.
// THAT
// The edited <input>s are sent to the browser.
TEST_P(AutofillAgentSubmissionTest,
FormlessOnNavigationAfterSomeInputsRemoved) {
LoadHTML(R"(
Name: <input type='text' id='name'><br>
Address: <input type='text' id='address'>
)");
SimulateUserInputChangeForElementById("name", "Ariel");
SimulateUserInputChangeForElementById("address", "Atlantica");
if (prefer_saved_form()) {
EXPECT_CALL(autofill_driver(),
FormSubmitted(AllOf(FieldsAre(HasFieldIdAttribute(u"name"),
HasFieldIdAttribute(u"address")),
FieldsAre(HasValue(u"Ariel"),
HasValue(u"Atlantica"))),
_));
} else {
EXPECT_CALL(autofill_driver(),
FormSubmitted(AllOf(FieldsAre(HasFieldIdAttribute(u"address")),
FieldsAre(HasValue(u"Atlantica"))),
_));
}
// Remove element that the user did not interact with last.
ExecuteJavaScriptForTests(R"(document.getElementById('name').remove();)");
// Simulate page navigation.
autofill_agent().OnFormSubmission(
mojom::SubmissionSource::PROBABLY_FORM_SUBMITTED,
/*submitted_form_element=*/std::nullopt);
}
// Test that in the scenario that:
// - The user autofills a form which dynamically removes -
// during autofill - `AutofillAgent::last_queried_element_` from the DOM
// hierarchy.
// THAT
// - Inferred form submission as a result of the page removing the <form> from
// the DOM hierarchy does not send fields which were removed from the DOM
// hierarchy at autofill time.
TEST_P(AutofillAgentSubmissionTest,
OnInferredFormSubmissionAfterAutofillRemovesLastQueriedElement) {
LoadHTML(R"(
<form id="form">
<input id="input1">
<input id="input2" onchange="document.getElementById('input1').remove();">
</form>
)");
blink::WebFormElement form_element =
GetWebElementById("form").DynamicTo<blink::WebFormElement>();
ASSERT_TRUE(form_element);
std::optional<FormData> form = form_util::ExtractFormData(
GetDocument(), form_element, autofill_agent().field_data_manager(),
kCallTimerStateDummy, /*button_titles_cache=*/nullptr);
ASSERT_TRUE(form.has_value());
std::vector<blink::WebFormControlElement> field_elements =
form_element.GetFormControlElements();
for (const blink::WebFormControlElement& field_element : field_elements) {
ASSERT_EQ(field_element.GetAutofillState(),
blink::WebAutofillState::kNotFilled);
}
for (FormFieldData& field : test_api(*form).fields()) {
field.set_value(field.id_attribute() + u" autofilled");
field.set_is_autofilled(true);
}
// Update `AutofillAgent::last_queried_element_`.
static_cast<content::RenderFrameObserver*>(&autofill_agent())
->FocusedElementChanged(field_elements[0]);
autofill_agent().ApplyFieldsAction(mojom::FormActionType::kFill,
mojom::ActionPersistence::kFill,
GetFieldsForFilling({*form}));
for (const blink::WebFormControlElement& field_element : field_elements) {
ASSERT_EQ(field_element.GetAutofillState(),
blink::WebAutofillState::kAutofilled);
}
EXPECT_CALL(autofill_driver(),
FormSubmitted(AllOf(FieldsAre(HasFieldIdAttribute(u"input2")),
FieldsAre(HasValue(u"input2 autofilled"))),
_));
ExecuteJavaScriptForTests(R"(document.getElementById('form').remove();)");
autofill_agent().OnFormSubmission(mojom::SubmissionSource::XHR_SUCCEEDED,
/*submitted_form_element=*/std::nullopt);
}
class AutofillAgentTestNavigationReset : public AutofillAgentTest {
public:
std::unique_ptr<AutofillAgent> CreateAutofillAgent(
content::RenderFrame* render_frame,
std::unique_ptr<PasswordAutofillAgent> password_autofill_agent,
std::unique_ptr<PasswordGenerationAgent> password_generation_agent,
blink::AssociatedInterfaceRegistry* associated_interfaces) override {
return std::make_unique<MockAutofillAgent>(
render_frame, std::move(password_autofill_agent),
std::move(password_generation_agent), associated_interfaces);
}
MockAutofillAgent& autofill_agent() {
return static_cast<MockAutofillAgent&>(AutofillAgentTest::autofill_agent());
}
};
TEST_F(AutofillAgentTestNavigationReset, NavigationResetsIsDomContentLoaded) {
std::vector<bool> is_dom_content_loaded;
EXPECT_CALL(autofill_agent(), DidDispatchDOMContentLoadedEvent)
.WillRepeatedly([&] {
is_dom_content_loaded.push_back(
test_api(autofill_agent()).is_dom_content_loaded());
autofill_agent().OverriddenDidDispatchDOMContentLoadedEvent();
is_dom_content_loaded.push_back(
test_api(autofill_agent()).is_dom_content_loaded());
});
LoadHTML(R"(Hello world)");
LoadHTML(R"(Hello world)");
EXPECT_THAT(is_dom_content_loaded, ElementsAre(false, true, false, true));
}
// Test fixture for FocusedElementChanged().
class AutofillAgentTestFocus : public AutofillAgentTest {
public:
// A permutation of the fields. Cycling through these fields guarantees a
// diverse collection of transitions:
// - [un]owned -> [un]owned (all four combinations),
// - <input>, <select>, <textarea>, contenteditable
static constexpr std::array kPermutationOfFields = {
"owned_field", "owned_select", "unowned_field", "contenteditable",
"owned_field2", "unowned_field", "unowned_select", "owned_select2"};
void SetUp() override {
AutofillAgentTest::SetUp();
LoadHTML(R"(
<html>
<div id=uneditable></div>
<div id=contenteditable contenteditable></div>
<input id=unowned_field>
<select id=unowned_select><option>Something</option></select>
<form>
<input id=owned_field>
<select id=owned_select><option>Something</option></select>
</form>
<form>
<textarea id=owned_field2></textarea>
<select id=owned_select2><option>Something</option></select>
</form>
)");
for (std::string_view id : kPermutationOfFields) {
ASSERT_TRUE(GetWebElementById(id));
}
}
void FocusedElementChanged(blink::WebElement e) {
test_api(autofill_agent()).FocusedElementChanged(e);
task_environment_.RunUntilIdle();
}
void FocusedElementChanged(std::string_view id) {
blink::WebElement e = GetWebElementById(id);
ASSERT_TRUE(e) << "Field " << id << " doesn't exist";
FocusedElementChanged(e);
}
};
// Tests that when the focus moves from field to field, FocusedElementChanged()
// fires FocusOnFormField() and FocusOnNonFormField().
TEST_F(AutofillAgentTestFocus, FireFocusEventsWhenCyclingThroughFields) {
testing::MockFunction<void(std::string_view)> checkpoint;
{
testing::InSequence s;
// Moves the focus one field to another.
for (std::string_view id : kPermutationOfFields) {
EXPECT_CALL(checkpoint, Call(id));
EXPECT_CALL(autofill_driver(), FocusOnNonFormField).Times(0);
EXPECT_CALL(autofill_driver(),
FocusOnFormField(_, GetFieldRendererIdById(id)));
}
}
for (std::string_view id : kPermutationOfFields) {
checkpoint.Call(id);
FocusedElementChanged(id);
}
}
// Tests that when the focus switches between an uneditable <div> and
// a field, FocusedElementChanged() fires FocusOnFormField() and
// FocusOnNonFormField().
TEST_F(AutofillAgentTestFocus,
FireFocusEventsWhenSwitchingBetweenFieldAndNonField) {
testing::MockFunction<void(std::string_view)> checkpoint;
{
testing::InSequence s;
for (std::string_view id : kPermutationOfFields) {
EXPECT_CALL(checkpoint, Call("uneditable"));
EXPECT_CALL(autofill_driver(), FocusOnNonFormField);
EXPECT_CALL(autofill_driver(), FocusOnFormField).Times(0);
EXPECT_CALL(checkpoint, Call(id));
EXPECT_CALL(autofill_driver(), FocusOnNonFormField).Times(0);
EXPECT_CALL(autofill_driver(),
FocusOnFormField(_, GetFieldRendererIdById(id)));
}
}
for (std::string_view id : kPermutationOfFields) {
checkpoint.Call("uneditable");
FocusedElementChanged("uneditable");
checkpoint.Call(id);
FocusedElementChanged(id);
}
}
// Tests that FocusedElementChanged() treats null as a non-FormField.
TEST_F(AutofillAgentTestFocus, FireFocusEventsForNullElement) {
testing::MockFunction<void(std::string_view)> checkpoint;
{
testing::InSequence s;
EXPECT_CALL(checkpoint, Call("owned_field"));
EXPECT_CALL(autofill_driver(), FocusOnFormField);
EXPECT_CALL(checkpoint, Call("null"));
EXPECT_CALL(autofill_driver(), FocusOnNonFormField);
EXPECT_CALL(checkpoint, Call("contenteditable"));
EXPECT_CALL(autofill_driver(), FocusOnFormField);
EXPECT_CALL(checkpoint, Call("null"));
EXPECT_CALL(autofill_driver(), FocusOnNonFormField);
}
checkpoint.Call("owned_field");
FocusedElementChanged("owned_field");
checkpoint.Call("null");
FocusedElementChanged(blink::WebElement());
checkpoint.Call("contenteditable");
FocusedElementChanged("contenteditable");
checkpoint.Call("null");
FocusedElementChanged(blink::WebElement());
}
// This test fixture initializes the agent to use platform autofill. The agent
// expects the client counterpart to forward requests to the platform instead of
// using an embedder-specific implementation. This behavior matches Android
// Autofill in WebViews and 3P Mode in Chrome.
class AutofillAgentTestUsingPlatformAutofill : public AutofillAgentTest {
public:
std::unique_ptr<AutofillAgent> CreateAutofillAgent(
content::RenderFrame* render_frame,
std::unique_ptr<PasswordAutofillAgent> password_autofill_agent,
std::unique_ptr<PasswordGenerationAgent> password_generation_agent,
blink::AssociatedInterfaceRegistry* associated_interfaces) override {
EnablePlatformAutofillForFrame(render_frame);
return std::make_unique<AutofillAgent>(
render_frame, std::move(password_autofill_agent),
std::move(password_generation_agent), associated_interfaces);
}
};
// Tests that the agent in 3P mode doesn't fill insecure forms.
TEST_F(AutofillAgentTestUsingPlatformAutofill, InactiveWithoutSecureContext) {
EXPECT_CALL(autofill_driver(), FormsSeen);
LoadHTMLWithUrlOverride("<body><form><input id=ff></form></body>",
"http://example.com"); // Insecure context!
WaitForFormsSeen();
EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(0);
autofill_agent().TriggerSuggestions(
GetFieldRendererIdById("ff"),
AutofillSuggestionTriggerSource::kFormControlElementClicked);
task_environment_.RunUntilIdle();
}
// Tests that the agent in 3P mode does fill secure forms.
TEST_F(AutofillAgentTestUsingPlatformAutofill,
AskForValuesToFillWithSecureContext) {
EXPECT_CALL(autofill_driver(), FormsSeen);
LoadHTMLWithUrlOverride("<body><form><input id=ff></form></body>",
"https://example.com"); // Needs secure context!
WaitForFormsSeen();
EXPECT_CALL(autofill_driver(), AskForValuesToFill);
autofill_agent().TriggerSuggestions(
GetFieldRendererIdById("ff"),
AutofillSuggestionTriggerSource::kFormControlElementClicked);
task_environment_.RunUntilIdle();
}
// Test fixture for caret position extraction and movement detection.
class AutofillAgentTestCaret
: public AutofillAgentTest,
public ::testing::WithParamInterface<FormControlType> {
public:
FormControlType form_control_type() const { return GetParam(); }
void SetUp() override {
AutofillAgentTest::SetUp();
switch (form_control_type()) {
case FormControlType::kContentEditable:
LoadHTML(
R"(<div id=f contenteditable
style="width: 10em; height: 3ex;">012345</div>)");
break;
case FormControlType::kInputText:
LoadHTML(R"(<input id=f value=012345>)");
break;
case FormControlType::kTextArea:
LoadHTML(R"(<textarea id=f>012345</textarea>)");
break;
default:
NOTREACHED();
}
}
blink::WebElement GetElement() { return GetWebElementById("f"); }
void TriggerAskForValuesToFill() {
switch (form_control_type()) {
case FormControlType::kContentEditable:
test_api(autofill_agent())
.ShowSuggestionsForContentEditable(GetElement(), {});
break;
case FormControlType::kInputText:
case FormControlType::kTextArea:
test_api(autofill_agent())
.ShowSuggestions(
GetElement().DynamicTo<blink::WebFormControlElement>(),
AutofillSuggestionTriggerSource::kFormControlElementClicked,
/*form_cache=*/{},
/*password_request=*/std::nullopt);
break;
default:
NOTREACHED();
}
task_environment_.RunUntilIdle();
}
void SetCaret(int begin, int end, base::TimeDelta pause_for) {
switch (form_control_type()) {
case FormControlType::kContentEditable:
ExecuteJavaScriptForTests(base::StringPrintf(
R"(var c = document.getElementById('f').firstChild;
document.getSelection().setBaseAndExtent(c, %d, c, %d);)",
begin, end));
break;
case FormControlType::kInputText:
case FormControlType::kTextArea:
ExecuteJavaScriptForTests(base::StringPrintf(
R"(document.getElementById('f').setSelectionRange(%d, %d);)", begin,
end));
break;
default:
NOTREACHED();
}
task_environment_.FastForwardBy(pause_for);
}
};
INSTANTIATE_TEST_SUITE_P(AutofillAgentTest,
AutofillAgentTestCaret,
::testing::Values(FormControlType::kTextArea,
FormControlType::kContentEditable));
// Tests that AskForValuesToFill() is parameterized with the caret position.
TEST_P(AutofillAgentTestCaret, AskForValuesToFillContainsCaret) {
FormData form;
gfx::Rect caret_bounds;
EXPECT_CALL(autofill_driver(), AskForValuesToFill)
.WillOnce(DoAll(SaveArg<0>(&form), SaveArg<2>(&caret_bounds)));
Focus("f");
TriggerAskForValuesToFill();
EXPECT_FALSE(form.fields()[0].bounds().IsEmpty());
EXPECT_FALSE(caret_bounds.origin().IsOrigin());
EXPECT_GT(caret_bounds.height(), 0);
EXPECT_TRUE(form.fields()[0].bounds().Contains(gfx::RectF(caret_bounds)));
}
// Tests that CaretMovedInFormField() is fired for each caret movement, provided
// there's enough time between the movements.
TEST_P(AutofillAgentTestCaret, MovingCaretSlowlyFiresEvent) {
std::array<FormData, 3> forms;
std::array<gfx::Rect, 3> caret_bounds;
testing::MockFunction<void(std::string_view)> checkpoint;
{
testing::InSequence s;
EXPECT_CALL(checkpoint, Call("focus"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField)
.WillOnce(DoAll(SaveArg<0>(&forms[0]), SaveArg<2>(&caret_bounds[0])));
EXPECT_CALL(checkpoint, Call("first move"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField)
.WillOnce(DoAll(SaveArg<0>(&forms[1]), SaveArg<2>(&caret_bounds[1])));
EXPECT_CALL(checkpoint, Call("second move"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField)
.WillOnce(DoAll(SaveArg<0>(&forms[2]), SaveArg<2>(&caret_bounds[2])));
EXPECT_CALL(checkpoint, Call("done"));
}
checkpoint.Call("focus");
Focus("f");
checkpoint.Call("first move");
SetCaret(1, 1, /*pause_for=*/base::Seconds(1));
checkpoint.Call("second move");
SetCaret(2, 2, /*pause_for=*/base::Seconds(1));
checkpoint.Call("done");
EXPECT_TRUE(
forms[0].fields()[0].bounds().Contains(gfx::RectF(caret_bounds[0])));
EXPECT_TRUE(
forms[1].fields()[0].bounds().Contains(gfx::RectF(caret_bounds[1])));
EXPECT_TRUE(
forms[2].fields()[0].bounds().Contains(gfx::RectF(caret_bounds[2])));
EXPECT_FALSE(caret_bounds[0].origin().IsOrigin());
EXPECT_FALSE(caret_bounds[1].origin().IsOrigin());
EXPECT_FALSE(caret_bounds[2].origin().IsOrigin());
EXPECT_NE(caret_bounds[0], caret_bounds[1]);
EXPECT_NE(caret_bounds[0], caret_bounds[2]);
EXPECT_NE(caret_bounds[1], caret_bounds[2]);
}
// Tests that CaretMovedInFormField() is fired in a throttled manner when the
// caret moves fast.
TEST_P(AutofillAgentTestCaret, MovingCaretFastThrottlesEvent) {
testing::MockFunction<void(std::string_view)> checkpoint;
{
testing::InSequence s;
EXPECT_CALL(checkpoint, Call("focus"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField);
EXPECT_CALL(checkpoint, Call("first move"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField);
EXPECT_CALL(checkpoint, Call("second move is ignored"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField).Times(0);
EXPECT_CALL(checkpoint, Call("third move is throttled"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField).Times(0);
EXPECT_CALL(checkpoint, Call("timer expires"));
EXPECT_CALL(autofill_driver(), CaretMovedInFormField);
EXPECT_CALL(checkpoint, Call("done"));
}
checkpoint.Call("focus");
Focus("f");
checkpoint.Call("first move");
SetCaret(1, 1, /*pause_for=*/base::Milliseconds(1));
checkpoint.Call("second move is ignored");
SetCaret(2, 2, /*pause_for=*/base::Milliseconds(1));
checkpoint.Call("third move is throttled");
SetCaret(3, 3, /*pause_for=*/base::Milliseconds(1));
checkpoint.Call("timer expires");
task_environment_.FastForwardBy(base::Seconds(1));
checkpoint.Call("done");
}
// Tests that selecting text fires CaretMovedInFormField() with the text
// selection.
TEST_P(AutofillAgentTestCaret, SelectionFiresEvent) {
testing::MockFunction<void(std::string_view)> checkpoint;
{
testing::InSequence s;
EXPECT_CALL(checkpoint, Call("focus"));
EXPECT_CALL(autofill_driver(),
CaretMovedInFormField(FieldsAre(HasSelectedText(u"")), _, _));
EXPECT_CALL(checkpoint, Call("selection"));
EXPECT_CALL(
autofill_driver(),
CaretMovedInFormField(FieldsAre(HasSelectedText(u"123")), _, _));
EXPECT_CALL(checkpoint, Call("done"));
}
checkpoint.Call("focus");
Focus("f");
checkpoint.Call("selection");
SetCaret(1, 4, /*pause_for=*/base::Seconds(1));
checkpoint.Call("done");
}
// Tests fixture for click handling.
class AutofillAgentTestClick
: public AutofillAgentTest,
public ::testing::WithParamInterface<const char*> {
public:
const char* field_html() const { return GetParam(); }
void SetUp() override {
AutofillAgentTest::SetUp();
// The DIV and SPAN dimensions are chosen so that
// - an empty DIV is clickable and
// - clicking on a non-empty DIV hits the node (a text node or SPAN) inside
// that DIV.
LoadHTML(base::StringPrintf(R"(<html>
<style>
div { width: 5em; height: 2ex; }
</style>
<body>
<div id=other></div>
%s)",
field_html()));
}
};
INSTANTIATE_TEST_SUITE_P(
AutofillAgentTest,
AutofillAgentTestClick,
::testing::Values(R"(<input id=f>)",
R"(<textarea id=f></textarea>)",
R"(<div contenteditable id=f></div>)",
R"(<div contenteditable id=f>Hello world</div>)",
R"(<div contenteditable id=f><div></div></div>)"));
// Tests that clicking on a field triggers AskForValuesToFillOnClick().
// TODO(crbug.com/342126797): Fix Android's OnAskForValuesToFill() event.
#if !BUILDFLAG(IS_ANDROID)
#define MAYBE_AskForValuesToFillOnClick AskForValuesToFillOnClick
#else
#define MAYBE_AskForValuesToFillOnClick DISABLED_AskForValuesToFillOnClick
#endif
TEST_P(AutofillAgentTestClick, MAYBE_AskForValuesToFillOnClick) {
testing::MockFunction<void(std::string_view)> checkpoint;
{
testing::InSequence s;
FieldRendererId field = GetFieldRendererIdById("f");
EXPECT_CALL(checkpoint, Call("click on field"));
EXPECT_CALL(autofill_driver(),
AskForValuesToFill(_, field, _, _, Eq(std::nullopt)));
EXPECT_CALL(checkpoint, Call("click on field"));
EXPECT_CALL(autofill_driver(),
AskForValuesToFill(_, field, _, _, Eq(std::nullopt)));
EXPECT_CALL(checkpoint, Call("click outside of field"));
EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(0);
EXPECT_CALL(checkpoint, Call("right click on field"));
EXPECT_CALL(autofill_driver(), AskForValuesToFill).Times(0);
EXPECT_CALL(
autofill_driver(),
AskForValuesToFill(
_, _, _,
AutofillSuggestionTriggerSource::kTextareaFocusedWithoutClick,
Eq(std::nullopt)))
.Times(AtMost(1));
}
// Makes sure the next AskForValuesToFill() event is not throttled in
// AutofillAgent.
auto skip_throttle = [this]() {
task_environment_.FastForwardBy(base::Seconds(1));
};
WaitForFormsSeen();
skip_throttle();
checkpoint.Call("click on field");
Click("f");
skip_throttle();
checkpoint.Call("click on field");
Click("f");
skip_throttle();
checkpoint.Call("click outside of field");
Click("other");
skip_throttle();
checkpoint.Call("right click on field");
RightClick("f");
}
// Tests that DOMContentLoaded() emits a metric.
TEST_F(AutofillAgentTest, DOMContentLoadedEmitsMetric) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<p>Hello world</p>
)");
EXPECT_THAT(histogram_tester.GetAllSamples(
"Autofill.DOMContentLoadedInOutermostMainFrame"),
base::BucketsAre(base::Bucket(true, 1), base::Bucket(false, 0)));
}
} // namespace
} // namespace autofill