blob: a011787e11462669630c255a73cbf9ca2a4dd8b9 [file] [log] [blame]
// Copyright 2019 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_cache.h"
#include <algorithm>
#include <optional>
#include <string_view>
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.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/focus_test_utils.h"
#include "components/autofill/content/renderer/form_autofill_util.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_test_api.h"
#include "components/autofill/core/common/form_field_data.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#include "content/public/test/render_view_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/web/web_document.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_select_element.h"
using blink::WebDocument;
using blink::WebElement;
using testing::AllOf;
using testing::Each;
using testing::ElementsAre;
using testing::Field;
using testing::IsEmpty;
using testing::Property;
using testing::SizeIs;
using testing::UnorderedElementsAre;
namespace autofill {
namespace {
using CheckStatus = FormFieldData::CheckStatus;
auto HasId(FormRendererId expected_id) {
return Property("renderer_id", &FormData::renderer_id, expected_id);
}
auto HasName(std::string_view expected_name) {
return Property("name", &FormData::name, base::ASCIIToUTF16(expected_name));
}
auto IsToken(FrameToken expected_token, int expected_predecessor) {
return AllOf(
Field(&FrameTokenWithPredecessor::token, expected_token),
Field(&FrameTokenWithPredecessor::predecessor, expected_predecessor));
}
const FormData* GetFormByName(const std::vector<FormData>& forms,
std::string_view name) {
for (const FormData& form : forms) {
if (form.name() == base::ASCIIToUTF16(name)) {
return &form;
}
}
return nullptr;
}
class FormCacheBrowserTest : public test::AutofillRendererTest {
public:
static constexpr CallTimerState kCallTimerStateDummy = {
.call_site = CallTimerState::CallSite::kExtractForms,
.last_autofill_agent_reset = {},
.last_dom_content_loaded = {},
};
~FormCacheBrowserTest() override = default;
void SetUp() override {
test::AutofillRendererTest::SetUp();
form_cache_.emplace(&autofill_agent());
}
void TearDown() override {
form_cache_.reset();
test::AutofillRendererTest::TearDown();
}
FormCache::UpdateFormCacheResult UpdateFormCache() {
return form_cache_->UpdateFormCache(GetFieldDataManager(),
kCallTimerStateDummy);
}
size_t num_extracted_forms() {
return std::ranges::count_if(form_cache_->extracted_forms(),
[](const auto& id_and_form) {
const auto& [id, form] = id_and_form;
return form != nullptr;
});
}
FieldDataManager& GetFieldDataManager() const {
return *field_data_manager_.get();
}
test::FocusTestUtils& focus_test_utils() { return focus_test_utils_; }
private:
scoped_refptr<FieldDataManager> field_data_manager_ =
base::MakeRefCounted<FieldDataManager>();
test::FocusTestUtils focus_test_utils_{
base::BindRepeating(&FormCacheBrowserTest::ExecuteJavaScriptForTests,
base::Unretained(this))};
// The subject of this test fixture. The fixture inherits from
// AutofillRendererTest because we need an AutofillAgent to initialize a
// FormCache. That AutofillAgent has a FormCache on its own, but we define a
// separate one for this test fixture because the `AutofillAgent`'s cache is
// used and populated by `AutofillAgent`.
std::optional<FormCache> form_cache_;
};
TEST_F(FormCacheBrowserTest, UpdatedForms) {
LoadHTML(R"(
<form id="form1">
<input type="text" name="foo1">
<input type="text" name="foo2">
<input type="text" name="foo3">
</form>
<input type="text" name="unowned_element">
)");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
EXPECT_TRUE(forms.removed_forms.empty());
const FormData* form1 = GetFormByName(forms.updated_forms, "form1");
ASSERT_TRUE(form1);
EXPECT_EQ(3u, form1->fields().size());
EXPECT_TRUE(form1->child_frames().empty());
const FormData* unowned_form = GetFormByName(forms.updated_forms, "");
ASSERT_TRUE(unowned_form);
EXPECT_EQ(1u, unowned_form->fields().size());
EXPECT_TRUE(unowned_form->child_frames().empty());
}
TEST_F(FormCacheBrowserTest, RemovedForms) {
LoadHTML(R"(
<form id="form1">
<input type="text" name="foo1">
<input type="text" name="foo2">
<input type="text" name="foo3">
</form>
<form id="form2">
<input type="text" name="foo1">
<input type="text" name="foo2">
<input type="text" name="foo3">
</form>
<input type="text" id="unowned_element">
)");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1"),
HasName("form2")));
EXPECT_TRUE(forms.removed_forms.empty());
std::vector<FormRendererId> forms_renderer_id;
for (const FormData& form : forms.updated_forms) {
if (form.renderer_id() != FormRendererId()) {
forms_renderer_id.push_back(form.renderer_id());
}
}
ASSERT_EQ(forms_renderer_id.size(), 2u);
ExecuteJavaScriptForTests(R"(
document.getElementById("form1").remove();
document.getElementById("form2").innerHTML = "";
)");
forms = UpdateFormCache();
EXPECT_TRUE(forms.updated_forms.empty());
EXPECT_THAT(forms.removed_forms,
UnorderedElementsAre(forms_renderer_id[0], forms_renderer_id[1]));
ExecuteJavaScriptForTests(R"(
document.getElementById("unowned_element").remove();
)");
forms = UpdateFormCache();
EXPECT_TRUE(forms.updated_forms.empty());
EXPECT_THAT(forms.removed_forms, ElementsAre(FormRendererId()));
ExecuteJavaScriptForTests(R"(
document.getElementById("form2").innerHTML = `
<input type="text" name="foo1">
<input type="text" name="foo2">
<input type="text" name="foo3">
`;
)");
forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms, ElementsAre(HasName("form2")));
EXPECT_TRUE(forms.removed_forms.empty());
ExecuteJavaScriptForTests(R"(
document.getElementById("form2").innerHTML = `
<input type="text" name="foo1">
<input type="text" name="foo2">
<input type="text" name="foo3">
<input type="text" name="foo4">
`;
)");
forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms, ElementsAre(HasName("form2")));
EXPECT_TRUE(forms.removed_forms.empty());
}
// Test if the form gets re-extracted after a label change.
TEST_F(FormCacheBrowserTest, ExtractFormAfterDynamicFieldChange) {
LoadHTML(R"(
<form id="f"><input></form>
<form id="g"> <label id="label">Name</label><input></form>
)");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasName("f"), HasName("g")));
EXPECT_TRUE(forms.removed_forms.empty());
ExecuteJavaScriptForTests(R"(
document.getElementById("label").innerHTML = "Last Name";
)");
forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms, ElementsAre(HasName("g")));
EXPECT_TRUE(forms.removed_forms.empty());
}
TEST_F(FormCacheBrowserTest, ExtractFrames) {
LoadHTML(R"(
<form id="form1">
<iframe id="frame1"></iframe>
</form>
<iframe id="frame2"></iframe>
)");
FrameToken frame1_token = GetFrameToken(GetDocument(), "frame1");
FrameToken frame2_token = GetFrameToken(GetDocument(), "frame2");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
EXPECT_TRUE(forms.removed_forms.empty());
const FormData* form1 = GetFormByName(forms.updated_forms, "form1");
ASSERT_TRUE(form1);
EXPECT_TRUE(form1->fields().empty());
EXPECT_THAT(form1->child_frames(), ElementsAre(IsToken(frame1_token, -1)));
const FormData* unowned_form = GetFormByName(forms.updated_forms, "");
ASSERT_TRUE(unowned_form);
EXPECT_TRUE(unowned_form->fields().empty());
EXPECT_THAT(unowned_form->child_frames(),
ElementsAre(AllOf(IsToken(frame2_token, -1))));
}
TEST_F(FormCacheBrowserTest, ExtractFormsTwice) {
LoadHTML(R"(
<form id="form1">
<input type="text" name="foo1">
<input type="text" name="foo2">
<input type="text" name="foo3">
</form>
<input type="text" name="unowned_element">
)");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
EXPECT_TRUE(forms.removed_forms.empty());
forms = UpdateFormCache();
// As nothing has changed, there are no new or removed forms.
EXPECT_TRUE(forms.updated_forms.empty());
EXPECT_TRUE(forms.removed_forms.empty());
}
TEST_F(FormCacheBrowserTest, ExtractFramesTwice) {
LoadHTML(R"(
<form id="form1">
<iframe></iframe>
</form>
<iframe></iframe>
)");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
EXPECT_TRUE(forms.removed_forms.empty());
forms = UpdateFormCache();
// As nothing has changed, there are no new or removed forms.
EXPECT_TRUE(forms.updated_forms.empty());
EXPECT_TRUE(forms.removed_forms.empty());
}
// TODO(crbug.com/40144964) Adjust expectations when we omit invisible iframes.
TEST_F(FormCacheBrowserTest, ExtractFramesAfterVisibilityChange) {
LoadHTML(R"(
<form id="form1">
<iframe id="frame1" style="display: none;"></iframe>
<iframe id="frame2" style="display: none;"></iframe>
</form>
<iframe id="frame3" style="display: none;"></iframe>
)");
WebElement iframe1 = GetElementById(GetDocument(), "frame1");
WebElement iframe2 = GetElementById(GetDocument(), "frame2");
WebElement iframe3 = GetElementById(GetDocument(), "frame3");
auto GetSize = [](const WebElement& element) {
gfx::Rect bounds = element.BoundsInWidget();
return bounds.width() * bounds.height();
};
ASSERT_LE(GetSize(iframe1), 0);
ASSERT_LE(GetSize(iframe2), 0);
ASSERT_LE(GetSize(iframe3), 0);
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
EXPECT_TRUE(forms.removed_forms.empty());
iframe1.SetAttribute("style", "display: block;");
iframe2.SetAttribute("style", "display: block;");
iframe3.SetAttribute("style", "display: block;");
ASSERT_GT(GetSize(iframe1), 0);
ASSERT_GT(GetSize(iframe2), 0);
ASSERT_GT(GetSize(iframe3), 0);
forms = UpdateFormCache();
EXPECT_TRUE(forms.updated_forms.empty());
EXPECT_TRUE(forms.removed_forms.empty());
iframe2.SetAttribute("style", "display: none;");
iframe3.SetAttribute("style", "display: none;");
ASSERT_GT(GetSize(iframe1), 0);
ASSERT_LE(GetSize(iframe2), 0);
ASSERT_LE(GetSize(iframe3), 0);
forms = UpdateFormCache();
EXPECT_TRUE(forms.updated_forms.empty());
EXPECT_TRUE(forms.removed_forms.empty());
}
TEST_F(FormCacheBrowserTest, ExtractFormsAfterModification) {
LoadHTML(R"(
<form id="form1">
<input type="text" name="foo1">
<input type="text" name="foo2">
<input type="text" name="foo3">
</form>
<input type="text" name="unowned_element">
)");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
EXPECT_TRUE(forms.removed_forms.empty());
// Append an input element to the form and to the list of unowned inputs.
ExecuteJavaScriptForTests(R"(
var new_input_1 = document.createElement("input");
new_input_1.setAttribute("type", "text");
new_input_1.setAttribute("name", "foo4");
var form1 = document.getElementById("form1");
form1.appendChild(new_input_1);
var new_input_2 = document.createElement("input");
new_input_2.setAttribute("type", "text");
new_input_2.setAttribute("name", "unowned_element_2");
document.body.appendChild(new_input_2);
)");
forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("form1")));
EXPECT_TRUE(forms.removed_forms.empty());
const FormData* form1 = GetFormByName(forms.updated_forms, "form1");
ASSERT_TRUE(form1);
EXPECT_EQ(4u, form1->fields().size());
const FormData* unowned_form = GetFormByName(forms.updated_forms, "");
ASSERT_TRUE(unowned_form);
EXPECT_EQ(2u, unowned_form->fields().size());
}
// Tests that correct focus, change and blur events are emitted during the
// autofilling and clearing of the form with an initially focused element.
// TODO: crbug.com/40100455 - Move elsewhere; the test is not about FormCache.
TEST_F(FormCacheBrowserTest,
VerifyFocusAndBlurEventsAfterAutofillAndClearingWithFocusElement) {
LoadHTML(R"(<form id='myForm'>
<label>First Name:</label><input id='fname' name='0'><br>
<label>Last Name:</label> <input id='lname' name='1'><br>
</form>)");
focus_test_utils().SetUpFocusLogging();
focus_test_utils().FocusElement("fname");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_THAT(forms.updated_forms,
UnorderedElementsAre(HasId(FormRendererId()), HasName("myForm")));
EXPECT_TRUE(forms.removed_forms.empty());
std::vector<FormFieldData::FillData> values_to_fill;
values_to_fill.reserve(forms.updated_forms[0].fields().size());
for (const FormFieldData& field : forms.updated_forms[0].fields()) {
values_to_fill.emplace_back(field);
}
values_to_fill[0].value = u"John";
values_to_fill[0].is_autofilled = true;
values_to_fill[1].value = u"Smith";
values_to_fill[1].is_autofilled = true;
auto fname = GetFormControlElementById("fname");
// Simulate filling the form using Autofill.
form_util::ApplyFieldsAction(
GetDocument(), values_to_fill, mojom::FormActionType::kFill,
mojom::ActionPersistence::kFill, GetFieldDataManager());
// Expected Result in order:
// - from filling
// * Change fname
// * Blur fname
// * Focus lname
// * Change lname
// * Blur lname
// * Focus fname
EXPECT_EQ(focus_test_utils().GetFocusLog(GetDocument()), "c0b0f1c1b1f0");
}
// Test that the FormCache does not contain empty forms.
TEST_F(FormCacheBrowserTest, DoNotStoreEmptyForms) {
LoadHTML(R"(<form></form>)");
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_TRUE(forms.updated_forms.empty());
EXPECT_TRUE(forms.removed_forms.empty());
EXPECT_EQ(1u, GetDocument().GetTopLevelForms().size());
EXPECT_EQ(0u, num_extracted_forms());
}
// Test that the FormCache never contains more than |kMaxExtractableFields|
// non-empty extracted forms.
TEST_F(FormCacheBrowserTest, FormCacheSizeUpperBound) {
// Create a HTML page that contains `kMaxExtractableFields + 1` non-empty
// forms.
std::string html;
for (unsigned int i = 0; i < kMaxExtractableFields + 1; i++) {
html += "<form><input></form>";
}
LoadHTML(html.c_str());
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_EQ(forms.updated_forms.size(), kMaxExtractableFields);
EXPECT_TRUE(forms.removed_forms.empty());
EXPECT_EQ(kMaxExtractableFields + 1, GetDocument().GetTopLevelForms().size());
EXPECT_EQ(kMaxExtractableFields, num_extracted_forms());
}
// Test that FormCache::UpdateFormCache() limits the number of total fields by
// skipping any additional forms.
TEST_F(FormCacheBrowserTest, FieldLimit) {
std::string html;
for (unsigned int i = 0; i < kMaxExtractableFields + 1; i++) {
html += "<form><input></form>";
}
LoadHTML(html.c_str());
ASSERT_EQ(kMaxExtractableFields + 1, GetDocument().GetTopLevelForms().size());
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_EQ(kMaxExtractableFields, forms.updated_forms.size());
EXPECT_TRUE(forms.removed_forms.empty());
}
// Test that FormCache::UpdateFormCache() limits the number of total frames by
// clearing their frames and skipping the then-empty forms.
TEST_F(FormCacheBrowserTest, FrameLimit) {
std::string html;
for (unsigned int i = 0; i < kMaxExtractableChildFrames + 1; i++) {
html += "<form><iframe></iframe></form>";
}
LoadHTML(html.c_str());
ASSERT_EQ(kMaxExtractableChildFrames + 1,
GetDocument().GetTopLevelForms().size());
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_EQ(kMaxExtractableChildFrames, forms.updated_forms.size());
EXPECT_TRUE(forms.removed_forms.empty());
}
// Test that FormCache::UpdateFormCache() limits the number of total fields and
// total frames:
// - the forms [0, kMaxExtractableChildFrames) should be unchanged,
// - the forms [kMaxExtractableChildFrames, kMaxExtractableFields) should have
// empty FormData::child_frames,
// - the forms [kMaxExtractableFields, end) should be skipped.
TEST_F(FormCacheBrowserTest, FieldAndFrameLimit) {
// Ideally, the test would create `kMaxExtractableFields + 1` forms
// <form><input><iframe></iframe></form>
// But that many iframes seem to cause test timeouts. So only the first
// `kMaxExtractableChildFrames + 1` forms will get an <iframe>.
ASSERT_LE(kMaxExtractableChildFrames, kMaxExtractableFields);
constexpr size_t kNumFormsWithFrame = kMaxExtractableChildFrames + 1;
constexpr size_t kNumFormsWithoutFrame =
kMaxExtractableFields + 1 - kNumFormsWithFrame;
std::string html;
for (size_t i = 0; i < kNumFormsWithFrame; i++) {
html += "<form><input><iframe></iframe></form>";
}
for (size_t i = 0; i < kNumFormsWithoutFrame; i++) {
html += "<form><input></form>";
}
LoadHTML(html.c_str());
ASSERT_EQ(kMaxExtractableFields + 1,
GetDocument().GetElementsByHTMLTagName("form").length());
ASSERT_EQ(kMaxExtractableFields + 1,
GetDocument().GetElementsByHTMLTagName("input").length());
ASSERT_EQ(kMaxExtractableChildFrames + 1,
GetDocument().GetElementsByHTMLTagName("iframe").length());
FormCache::UpdateFormCacheResult forms = UpdateFormCache();
EXPECT_EQ(forms.updated_forms.size(), kMaxExtractableFields);
EXPECT_THAT(forms.updated_forms,
Each(Property("fields", &FormData::fields, SizeIs(1))));
EXPECT_THAT(
base::span(forms.updated_forms).first<kMaxExtractableChildFrames>(),
Each(Property("child_frames", &FormData::child_frames, SizeIs(1))));
EXPECT_THAT(
base::span(forms.updated_forms).subspan<kMaxExtractableChildFrames>(),
Each(Property("child_frames", &FormData::child_frames, IsEmpty())));
EXPECT_THAT(forms.removed_forms, IsEmpty());
}
// Tests that form extraction measures its total time.
TEST_F(FormCacheBrowserTest, UpdateFormCacheMeasuresTotalTime) {
base::HistogramTester histogram_tester;
LoadHTML(R"(
<input>
)");
// FormCache::UpdateFormCache() is called by AutofillAgent.
histogram_tester.ExpectTotalCount("Autofill.TimingPrecise.UpdateFormCache",
1);
histogram_tester.ExpectTotalCount(
"Autofill.TimingPrecise.UpdateFormCache.DidDispatchDomContentLoadedEvent",
1);
// / On pageload `AutofillAgent::DidDispatchDomContentLoadedEvent()` and
// `PasswordAutofillAgent::DidFinishLoad()` are called, each triggering form
// extraction.
histogram_tester.ExpectTotalCount("Autofill.TimingPrecise.ExtractFormData",
2);
histogram_tester.ExpectTotalCount(
"Autofill.TimingPrecise.ExtractFormData.UpdateFormCache", 1);
}
} // namespace
} // namespace autofill