blob: 57e4c93807201dce20453cd4765760cc0e3cbb69 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/autofill/autofill_flow_test_util.h"
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "chrome/browser/autofill/autofill_uitest.h"
#include "chrome/browser/translate/translate_test_utils.h"
#include "chrome/browser/ui/autofill/autofill_suggestion_controller.h"
#include "chrome/browser/ui/autofill/chrome_autofill_client.h"
#include "chrome/browser/ui/translate/translate_bubble_model.h"
#include "chrome/browser/ui/translate/translate_bubble_test_utils.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/common/autofill_util.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/gfx/geometry/point.h"
using base::ASCIIToUTF16;
using ::testing::_;
using ::testing::AssertionFailure;
using ::testing::AssertionResult;
using ::testing::AssertionSuccess;
namespace autofill {
namespace {
// Returns the center point of a DOM element.
gfx::Point GetCenter(const ElementExpr& e,
content::ToRenderFrameHost execution_target) {
std::string x_script = base::StringPrintf(
R"( const bounds = (%s).getBoundingClientRect();
Math.floor(bounds.left + bounds.width / 2))",
e->c_str());
std::string y_script = base::StringPrintf(
R"( const bounds = (%s).getBoundingClientRect();
Math.floor(bounds.top + bounds.height / 2)
)",
e->c_str());
int x = content::EvalJs(execution_target, x_script).ExtractInt();
int y = content::EvalJs(execution_target, y_script).ExtractInt();
return gfx::Point(x, y);
}
// Triggers a JavaScript event like 'focus' and waits for the event to happen.
[[nodiscard]] AssertionResult TriggerAndWaitForEvent(
const ElementExpr& e,
const std::string& event_name,
content::ToRenderFrameHost execution_target) {
std::string script = base::StringPrintf(
R"( new Promise(resolve => {
if (document.readyState === 'complete') {
function handler(e) {
e.target.removeEventListener(e.type, arguments.callee);
resolve(true);
}
const target = %s;
target.addEventListener('%s', handler);
target.%s();
} else {
resolve(false);
}
});
)",
e->c_str(), event_name.c_str(), event_name.c_str());
content::EvalJsResult result = content::EvalJs(execution_target, script);
if (!result.error.empty()) {
return AssertionFailure() << __func__ << "(): " << result.error;
} else if (false == result) {
return AssertionFailure()
<< __func__ << "(): couldn't trigger " << event_name << " on " << *e;
} else {
return AssertionSuccess();
}
}
// True iff `e` is the deepest active element in the given frame.
//
// "Deepest" refers to the shadow DOM: if an <input> in the shadow DOM is
// focused, then this <input> and the shadow host are active elements, but
// IsFocusedField() only returns true for the <input>.
bool IsFocusedField(const ElementExpr& e,
content::ToRenderFrameHost execution_target) {
std::string script = base::StringPrintf(
"const e = (%s); e === e.getRootNode().activeElement", e->c_str());
return true == content::EvalJs(execution_target, script);
}
// Unfocuses the currently focused field.
[[nodiscard]] AssertionResult BlurFocusedField(
content::ToRenderFrameHost execution_target) {
std::string script = R"(
if (document.activeElement !== null)
document.activeElement.blur();
)";
return content::ExecJs(execution_target, script);
}
struct ShowAutofillSuggestionsParams {
ShowMethod show_method = ShowMethod::ByArrow();
int num_profile_suggestions = 1;
size_t max_tries = 5;
base::TimeDelta timeout = kAutofillFlowDefaultTimeout;
std::optional<content::ToRenderFrameHost> execution_target = {};
};
// A helper function for showing the popup in AutofillFlow().
// Consider using AutofillFlow() instead.
[[nodiscard]] AssertionResult ShowAutofillSuggestions(
const ElementExpr& e,
AutofillUiTest* test,
ShowAutofillSuggestionsParams p) {
constexpr auto kSuggest = ObservedUiEvents::kSuggestionsShown;
constexpr auto kPreview = ObservedUiEvents::kPreviewFormData;
content::ToRenderFrameHost execution_target =
p.execution_target.value_or(test->GetWebContents());
content::RenderFrameHost* rfh = execution_target.render_frame_host();
content::RenderWidgetHostView* view = rfh->GetView();
content::RenderWidgetHost* widget = view->GetRenderWidgetHost();
auto ArrowDown = [&](std::list<ObservedUiEvents> exp) {
constexpr auto kDown = ui::DomKey::ARROW_DOWN;
if (base::Contains(exp, ObservedUiEvents::kSuggestionsShown)) {
return test->SendKeyToPageAndWait(kDown, std::move(exp), p.timeout);
} else {
return test->SendKeyToPopupAndWait(kDown, std::move(exp), widget,
p.timeout);
}
};
auto Backspace = [&]() {
return test->SendKeyToPageAndWait(ui::DomKey::BACKSPACE, {}, p.timeout);
};
auto Char = [&](const std::string& code, std::list<ObservedUiEvents> exp) {
ui::DomCode dom_code = ui::KeycodeConverter::CodeStringToDomCode(code);
ui::DomKey dom_key;
ui::KeyboardCode keyboard_code;
CHECK(ui::DomCodeToUsLayoutDomKey(dom_code, ui::EF_SHIFT_DOWN, &dom_key,
&keyboard_code));
return test->SendKeyToPageAndWait(dom_key, dom_code, keyboard_code,
std::move(exp), p.timeout);
};
auto Click = [&](std::list<ObservedUiEvents> exp) {
gfx::Point point = view->TransformPointToRootCoordSpace(GetCenter(e, rfh));
test->test_delegate()->SetExpectations(
{ObservedUiEvents::kSuggestionsShown}, p.timeout);
content::SimulateMouseClickAt(test->GetWebContents(), 0,
blink::WebMouseEvent::Button::kLeft, point);
return test->test_delegate()->Wait();
};
// It seems that due to race conditions with Blink's layouting
// (crbug.com/1175735#c9), the below focus events are sometimes too early:
// Autofill closes the popup right away because it is outside of the content
// area. To work around this, we attempt to bring up the Autofill popup
// multiple times, with some delay.
testing::Message m;
m << __func__ << "(): with " << p.num_profile_suggestions
<< " profile suggestions.";
bool field_was_focused_initially = IsFocusedField(e, rfh);
for (size_t i = 1; i <= p.max_tries; ++i) {
m << "\nIteration " << i << "/" << p.max_tries << ". ";
// A Translate bubble may overlap with the Autofill popup, which causes
// flakiness. See crbug.com/1175735#c10.
// Also, the address-save prompts and others may overlap with the Autofill
// popup. So we preemptively close all bubbles, which however is not
// reliable on Windows.
translate::test_utils::CloseCurrentBubble(test->browser());
TryToCloseAllPrompts(test->GetWebContents());
if (i > 1) {
test->DoNothingAndWaitAndIgnoreEvents(p.timeout);
if (field_was_focused_initially) {
// The Autofill popup may have opened due to a severely delayed event on
// a slow bot. To reset the popup, we re-focus the field.
m << "Trying to re-focus the field. ";
if (AssertionResult b = BlurFocusedField(rfh); !b) {
m << b.message();
}
if (AssertionResult b = FocusField(e, rfh); !b) {
m << b.message();
}
}
}
bool has_preview = 0 < p.num_profile_suggestions;
if (p.show_method.arrow) {
// Press arrow down to open the popup and select first suggestion.
// Depending on the platform, this requires one or two arrow-downs.
if (!IsFocusedField(e, rfh)) {
return AssertionFailure()
<< m << "Field " << *e << " must be focused. ";
}
if (!ShouldAutoselectFirstSuggestionOnArrowDown()) {
if (AssertionResult b = ArrowDown({kSuggest}); !b) {
m << "Cannot trigger suggestions by first arrow: " << b.message();
continue;
}
if (AssertionResult b =
has_preview ? ArrowDown({kPreview}) : ArrowDown({});
!b) {
m << "Cannot select first suggestion by second arrow: "
<< b.message();
continue;
}
} else if (AssertionResult b = has_preview
? ArrowDown({kPreview, kSuggest})
: ArrowDown({kSuggest});
!b) {
m << "Cannot trigger and select first suggestion by arrow: "
<< b.message();
continue;
}
} else if (p.show_method.character) {
// Enter character to open the popup, but do not select an option.
// If necessary, delete past iterations character first.
if (!IsFocusedField(e, rfh)) {
return AssertionFailure()
<< m << "Field " << *e << " must be focused. ";
}
if (i > 1) {
if (AssertionResult b = Backspace(); !b) {
m << "Cannot undo past iteration's key: " << b.message();
}
}
std::string code = std::string("Key") + p.show_method.character;
if (AssertionResult b = Char(code, {kSuggest}); !b) {
m << "Cannot trigger suggestions by key: " << b.message();
continue;
}
} else if (p.show_method.click) {
// Click item to open the popup, but do not select an option.
if (AssertionResult b = Click({kSuggest}); !b) {
m << "Cannot trigger and select first suggestion by click: "
<< b.message();
continue;
}
}
LOG(WARNING) << (m << "Succeeded.");
return AssertionSuccess();
}
return AssertionFailure()
<< m << "Couldn't show Autofill suggestions on " << *e << ". ";
}
struct AutofillSuggestionParams {
int num_profile_suggestions = 1;
int current_index = 0;
int target_index = 0;
base::TimeDelta timeout = kAutofillFlowDefaultTimeout;
std::optional<content::ToRenderFrameHost> execution_target = {};
};
// A helper function for selecting a suggestion in AutofillFlow().
// Consider using AutofillFlow() instead.
[[nodiscard]] AssertionResult SelectAutofillSuggestion(
const ElementExpr& e,
AutofillUiTest* test,
AutofillSuggestionParams p) {
content::RenderWidgetHost* widget =
p.execution_target.value_or(test->GetWebContents())
.render_frame_host()
->GetView()
->GetRenderWidgetHost();
constexpr auto kPreview = ObservedUiEvents::kPreviewFormData;
auto ArrowDown = [&](std::list<ObservedUiEvents> exp) {
return test->SendKeyToPopupAndWait(ui::DomKey::ARROW_DOWN, std::move(exp),
widget, p.timeout);
};
for (int i = p.current_index + 1; i <= p.target_index; ++i) {
bool has_preview = i < p.num_profile_suggestions;
if (!(has_preview ? ArrowDown({kPreview}) : ArrowDown({}))) {
return AssertionFailure()
<< __func__ << "(): Couldn't go to " << i << "th suggestion with"
<< (has_preview ? "" : "out") << " preview";
}
}
return AssertionSuccess();
}
// A helper function for accepting a suggestion in AutofillFlow().
// Consider using AutofillFlow() instead.
[[nodiscard]] AssertionResult AcceptAutofillSuggestion(
const ElementExpr& e,
AutofillUiTest* test,
AutofillSuggestionParams p) {
content::RenderWidgetHost* widget =
p.execution_target.value_or(test->GetWebContents())
.render_frame_host()
->GetView()
->GetRenderWidgetHost();
// All attempts to accept Autofill suggestions using keyboard "ENTER"
// keystrokes will be ignored for the first 500ms after the popup is first
// shown. This overrides this threshold.
if (base::WeakPtr<AutofillSuggestionController> controller =
ChromeAutofillClient::FromWebContentsForTesting(
test->GetWebContents())
->suggestion_controller_for_testing()) {
controller->DisableThresholdForTesting(true);
}
constexpr auto kSuggestionsHidden = ObservedUiEvents::kSuggestionsHidden;
constexpr auto kFill = ObservedUiEvents::kFormDataFilled;
auto Enter = [&](std::list<ObservedUiEvents> exp) {
return test->SendKeyToPopupAndWait(ui::DomKey::ENTER, std::move(exp),
widget);
};
bool has_fill = p.target_index < p.num_profile_suggestions;
if (AssertionResult a = SelectAutofillSuggestion(e, test, p); !a) {
return a;
}
if (!(has_fill ? Enter({kFill, kSuggestionsHidden})
: Enter({kSuggestionsHidden}))) {
return AssertionFailure()
<< __func__ << "(): Couldn't accept to " << p.target_index
<< "th suggestion with" << (has_fill ? "" : "out") << " fill";
}
return AssertionSuccess();
}
} // namespace
// A helper function for focusing a field in AutofillFlow().
// Consider using AutofillFlow() instead.
[[nodiscard]] AssertionResult FocusField(
const ElementExpr& e,
content::ToRenderFrameHost execution_target) {
if (IsFocusedField(e, execution_target)) {
AssertionResult r = BlurFocusedField(execution_target);
if (!r) {
return r;
}
}
return TriggerAndWaitForEvent(e, "focus", execution_target);
}
[[nodiscard]] AssertionResult AutofillFlow(const ElementExpr& e,
AutofillUiTest* test,
AutofillFlowParams p) {
content::ToRenderFrameHost execution_target =
p.execution_target.value_or(test->GetWebContents());
if (p.do_focus) {
AssertionResult a = FocusField(e, execution_target);
if (!a) {
return a;
}
if (p.after_focus) {
p.after_focus.Run();
}
}
if (p.do_show) {
AssertionResult a = ShowAutofillSuggestions(
e, test,
{.show_method = p.show_method,
.num_profile_suggestions = p.num_profile_suggestions,
.max_tries = p.max_show_tries,
.timeout = p.timeout,
.execution_target = execution_target});
if (!a) {
return a;
}
if (p.after_show) {
p.after_show.Run();
}
}
if (p.do_select) {
AssertionResult a = SelectAutofillSuggestion(
e, test,
{.num_profile_suggestions = p.num_profile_suggestions,
.current_index = p.show_method.selects_first_suggestion() ? 0 : -1,
.target_index = p.target_index,
.timeout = p.timeout,
.execution_target = execution_target});
if (!a) {
return a;
}
if (p.after_select) {
p.after_select.Run();
}
}
if (p.do_accept) {
AssertionResult a = AcceptAutofillSuggestion(
e, test,
{.num_profile_suggestions = p.num_profile_suggestions,
.current_index = p.target_index,
.target_index = p.target_index,
.timeout = p.timeout,
.execution_target = execution_target});
if (!a) {
return a;
}
if (p.after_accept) {
p.after_accept.Run();
}
}
return AssertionSuccess();
}
} // namespace autofill