blob: b1a0fbb5ee515310491fa89c21bb05db560dea98 [file] [log] [blame]
// Copyright 2018 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/captured_sites_test_utils.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/base_switches.h"
#include "base/check_deref.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_string_value_serializer.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/autofill/autofill_uitest_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/browser/autofill_test_utils.h"
#include "components/autofill/core/browser/autofill_type.h"
#include "components/autofill/core/browser/data_model/autofill_profile.h"
#include "components/autofill/core/browser/data_model/credit_card.h"
#include "components/autofill/core/browser/test_autofill_clock.h"
#include "components/autofill/core/common/autofill_clock.h"
#include "components/javascript_dialogs/app_modal_dialog_controller.h"
#include "components/javascript_dialogs/app_modal_dialog_view.h"
#include "components/permissions/permission_request_manager.h"
#include "components/variations/variations_switches.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/browsing_data_remover_test_util.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "ipc/ipc_channel_factory.h"
#include "ipc/ipc_logging.h"
#include "ipc/ipc_message_macros.h"
#include "ipc/ipc_sync_message.h"
#include "third_party/zlib/google/compression_utils.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_codes.h"
using base::JSONParserOptions;
using base::JSONReader;
namespace {
// The command-line flag to specify the command file flag.
const char kCommandFileFlag[] = "command_file";
// The command line flag to turn on verbose WPR logging.
const char kWebPageReplayVerboseFlag[] = "wpr_verbose";
// The maximum amount of time to wait for Chrome to finish autofilling a form.
const base::TimeDelta kAutofillActionWaitForVisualUpdateTimeout =
base::Seconds(3);
// The number of tries the TestRecipeReplayer should perform when executing an
// Chrome Autofill action.
// Chrome Autofill can be flaky on some real-world pages. The Captured Site
// Automation Framework will retry an autofill action a couple times before
// concluding that Chrome Autofill does not work.
const int kAutofillActionNumRetries = 5;
// The public key hash for the certificate Web Page Replay (WPR) uses to serve
// HTTPS content.
// The Captured Sites Test Framework relies on WPR to serve captured site
// traffic. If a machine does not have the WPR certificate installed, Chrome
// will detect a server certificate validation failure when WPR serves Chrome
// HTTPS content. In response Chrome will block the WPR HTTPS content.
// The test framework avoids this problem by launching Chrome with the
// ignore-certificate-errors-spki-list flag set to the WPR certificate's
// public key hash. Doing so tells Chrome to ignore server certificate
// validation errors from WPR.
const char kWebPageReplayCertSPKI[] =
"PoNnQAwghMiLUPg1YNFtvTfGreNT8r9oeLEyzgNCJWc=";
const char kClockNotSetMessage[] =
"No AutofillClock override set from wpr archive: ";
// Check and return that the caller wants verbose WPR output (off by default).
bool IsVerboseWprLoggingEnabled() {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
return command_line && command_line->HasSwitch(kWebPageReplayVerboseFlag);
}
void PrintDebugInstructions(const base::FilePath& command_file_path) {
const char msg[] = R"(
*** INTERACTIVE DEBUGGING MODE ***
To proceed, you should create a named pipe:
$ mkfifo %1$s
and then write commands into it:
$ echo run >%1$s # unpauses execution
$ echo next 2 >%1$s # executes the next 2 actions
$ echo next -1 >%1$s # executes until the last action
$ echo skip -3 >%1$s # jumps back 3 actions
$ echo skip 4 >%1$s # skips the next 4 actions
$ echo where >%1$s # prints the current position
$ echo show -1 >%1$s # prints last 1 actions
$ echo show 1 >%1$s # prints next 1 actions
$ echo failure >%1$s # unpauses execution until failure
$ echo help >%1$s # prints this text
)";
VLOG(1) << base::StringPrintf(msg, command_file_path.AsUTF8Unsafe().c_str());
}
std::optional<autofill::FieldType> StringToFieldType(std::string_view str) {
static auto map = []() {
std::map<std::string_view, autofill::FieldType> map;
for (autofill::FieldType field_type : autofill::kAllFieldTypes) {
map[autofill::AutofillType(field_type).ToStringView()] = field_type;
}
for (autofill::HtmlFieldType html_field_type :
autofill::kAllHtmlFieldTypes) {
autofill::AutofillType field_type(html_field_type);
map[field_type.ToStringView()] = field_type.GetStorableType();
}
return map;
}();
auto it = map.find(str);
if (it == map.end()) {
return std::nullopt;
}
return it->second;
}
// Command types to control and debug execution.
// * The |kAbsoluteLimit| and |kRelativeLimit| commands indicate that
// execution shall not proceed if the next action's position is >= |param|
// or >= current_index + |param|, respectively.
// * The |kSkipAction| command jumps |param| actions forward or backward.
// * The |kShowAction| command prints the |param| previous (if < 0) or
// upcoming (if > 0) actions.
// * The |kWhereAmI| command prints the current execution position.
enum class ExecutionCommandType {
kAbsoluteLimit,
kRelativeLimit,
kSkipAction,
kShowAction,
kWhereAmI,
kRunUntilFailure
};
struct ExecutionCommand {
ExecutionCommandType type = ExecutionCommandType::kAbsoluteLimit;
int param = std::numeric_limits<int>::max();
};
// Blockingly reads the content of |command_file_path|, parses it into
// ExecutionCommands, and returns the result.
std::vector<ExecutionCommand> ReadExecutionCommands(
const base::FilePath& command_file_path) {
std::vector<ExecutionCommand> commands;
std::string command_lines;
if (base::ReadFileToString(command_file_path, &command_lines)) {
for (const auto command :
base::SplitStringPiece(command_lines, "\n", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
auto GetParamOr = [command](int default_value) {
size_t space = command.find(' ');
if (space == std::string_view::npos) {
return default_value;
}
int value;
if (!base::StringToInt(command.substr(space + 1), &value))
return default_value;
return value;
};
if (command.starts_with("run")) {
commands.push_back({ExecutionCommandType::kAbsoluteLimit,
std::numeric_limits<int>::max()});
} else if (command.starts_with("next")) {
commands.push_back(
{ExecutionCommandType::kRelativeLimit, GetParamOr(1)});
} else if (command.starts_with("skip")) {
commands.push_back({ExecutionCommandType::kSkipAction, GetParamOr(1)});
} else if (command.starts_with("show")) {
commands.push_back({ExecutionCommandType::kShowAction, GetParamOr(1)});
} else if (command.starts_with("where")) {
commands.push_back({ExecutionCommandType::kWhereAmI});
} else if (command.starts_with("failure")) {
commands.push_back({ExecutionCommandType::kRunUntilFailure});
// Same commands as for "run":
commands.push_back({ExecutionCommandType::kAbsoluteLimit,
std::numeric_limits<int>::max()});
} else if (command.starts_with("help")) {
PrintDebugInstructions(command_file_path);
}
}
}
return commands;
}
struct ExecutionState {
// The position of the next action to be executed.
int index = 0;
// The current bound on the execution.
int limit = std::numeric_limits<int>::max();
// The number of actions to be executed.
int length = 0;
// Whether to stop at a step if a failure is detected.
bool pause_on_failure = false;
};
// Blockingly reads the commands from |command_file_path| and executes them.
// Execution primarily means manipulation of the |execution_state|, particularly
// `execution_state.limit`.
ExecutionState ProcessCommands(ExecutionState execution_state,
const base::Value::List* action_list,
const base::FilePath& command_file_path) {
while (execution_state.limit <= execution_state.index) {
for (ExecutionCommand command : ReadExecutionCommands(command_file_path)) {
switch (command.type) {
case ExecutionCommandType::kAbsoluteLimit: {
execution_state.limit = command.param;
break;
}
case ExecutionCommandType::kRelativeLimit: {
if (command.param >= 0) {
execution_state.limit += command.param;
} else {
execution_state.limit = execution_state.length + command.param;
}
break;
}
case ExecutionCommandType::kSkipAction: {
execution_state.index += command.param;
execution_state.index = std::min(std::max(execution_state.index, 0),
execution_state.length - 1);
break;
}
case ExecutionCommandType::kShowAction: {
int min_index = execution_state.index + std::min(command.param, 0);
int max_index = execution_state.index + std::max(command.param, 0);
min_index = std::max(min_index, 0);
max_index = std::min(max_index, execution_state.length);
for (int i = min_index; i < max_index; ++i) {
VLOG(1) << "Action " << (i - execution_state.index) << ": "
<< (*action_list)[i].DebugString();
}
break;
}
case ExecutionCommandType::kWhereAmI: {
VLOG(1) << "Next action is at position " << execution_state.index
<< ", limit (excl) is at " << execution_state.limit
<< ", last (excl) is at " << execution_state.length;
break;
}
case ExecutionCommandType::kRunUntilFailure: {
VLOG(1) << "Will stop when a failure is found.";
execution_state.pause_on_failure = true;
break;
}
}
}
}
return execution_state;
}
struct AllowNull {
inline constexpr AllowNull() = default;
};
std::optional<std::string> FindPopulateString(
const base::Value::Dict& container,
std::string_view key_name,
absl::variant<std::string_view, AllowNull> key_descriptor) {
const std::string* value = container.FindString(key_name);
if (!value) {
if (absl::holds_alternative<std::string_view>(key_descriptor)) {
ADD_FAILURE() << "Failed to extract '"
<< absl::get<std::string_view>(key_descriptor)
<< "' string from container!";
}
return std::nullopt;
}
return *value;
}
std::optional<std::vector<std::string>> FindPopulateStringVector(
const base::Value::Dict& container,
std::string_view key_name,
absl::variant<std::string_view, AllowNull> key_descriptor) {
const base::Value::List* list = container.FindList(key_name);
if (!list) {
if (absl::holds_alternative<std::string_view>(key_descriptor)) {
ADD_FAILURE() << "Failed to extract '"
<< absl::get<std::string_view>(key_descriptor)
<< "' strings from container!";
}
return std::nullopt;
}
std::vector<std::string> strings;
for (const base::Value& item : *list) {
if (!item.is_string()) {
if (absl::holds_alternative<std::string_view>(key_descriptor)) {
ADD_FAILURE() << "Failed to extract element of '"
<< absl::get<std::string_view>(key_descriptor)
<< "' vector from container!";
}
return std::nullopt;
}
strings.push_back(item.GetString());
}
return strings;
}
} // namespace
namespace captured_sites_test_utils {
CapturedSiteParams::CapturedSiteParams() = default;
CapturedSiteParams::~CapturedSiteParams() = default;
CapturedSiteParams::CapturedSiteParams(const CapturedSiteParams& other) =
default;
std::ostream& operator<<(std::ostream& os, const CapturedSiteParams& param) {
if (param.scenario_dir.empty())
return os << "Site: " << param.site_name;
return os << "Scenario: " << param.scenario_dir
<< ", Site: " << param.site_name;
}
// Iterate through Autofill's Web Page Replay capture file directory to look
// for captures sites and automation recipe files. Return a list of sites for
// which recipe-based testing is available.
std::vector<CapturedSiteParams> GetCapturedSites(
const base::FilePath& replay_files_dir_path) {
std::vector<CapturedSiteParams> sites;
base::FilePath config_file_path =
replay_files_dir_path.AppendASCII("testcases.json");
std::string json_text;
if (!base::ReadFileToString(config_file_path, &json_text)) {
LOG(WARNING) << "Could not read json file: " << config_file_path;
return sites;
}
// Parse json text content to json value node.
auto value_with_error = JSONReader::ReadAndReturnValueWithError(
json_text, JSONParserOptions::JSON_PARSE_RFC);
if (!value_with_error.has_value()) {
LOG(WARNING) << "Could not load test config from json file: "
<< "`testcases.json` because: "
<< value_with_error.error().message;
return sites;
}
base::Value::Dict root_node = std::move(*value_with_error).TakeDict();
const base::Value::List* list_node = root_node.FindList("tests");
if (!list_node) {
LOG(WARNING) << "No tests found in `testcases.json` config";
return sites;
}
bool also_run_disabled = GTEST_FLAG_GET(also_run_disabled_tests);
for (auto& item_val : *list_node) {
if (!item_val.is_dict()) {
continue;
}
const base::Value::Dict& item = item_val.GetDict();
CapturedSiteParams param;
param.site_name = CHECK_DEREF(item.FindString("site_name"));
if (const std::string* scenario_dir = item.FindString("scenario_dir")) {
param.scenario_dir = *scenario_dir;
}
param.is_disabled = item.FindBool("disabled").value_or(false);
const std::optional<int> bug_number = item.FindInt("bug_number");
if (bug_number) {
param.bug_number = bug_number.value();
}
if (param.is_disabled && !also_run_disabled)
continue;
const std::string* expectation_string = item.FindString("expectation");
if (expectation_string && *expectation_string == "FAIL") {
param.expectation = kFail;
} else {
param.expectation = kPass;
}
// Check that a pair of .test and .wpr files exist - otherwise skip
base::FilePath file_name = replay_files_dir_path;
base::FilePath refresh_file_path =
replay_files_dir_path.AppendASCII("refresh");
if (!param.scenario_dir.empty()) {
file_name = file_name.AppendASCII(param.scenario_dir);
refresh_file_path = refresh_file_path.AppendASCII(param.scenario_dir);
}
file_name = file_name.AppendASCII(param.site_name);
refresh_file_path =
refresh_file_path.AppendASCII(param.site_name).AddExtensionASCII("wpr");
base::FilePath capture_file_path = file_name.AddExtensionASCII("wpr");
if (!base::PathExists(capture_file_path)) {
LOG(WARNING) << "Test `" << param.site_name
<< "` had no matching .wpr file";
continue;
}
base::FilePath recipe_file_path = file_name.AddExtensionASCII("test");
if (!base::PathExists(recipe_file_path)) {
LOG(WARNING) << "Test `" << param.site_name
<< "` had no matching .test file";
continue;
}
param.capture_file_path = capture_file_path;
param.recipe_file_path = recipe_file_path;
param.refresh_file_path = refresh_file_path;
sites.push_back(param);
}
return sites;
}
std::string FilePathToUTF8(const base::FilePath::StringType& str) {
#if BUILDFLAG(IS_WIN)
return base::WideToUTF8(str);
#else
return str;
#endif
}
std::optional<base::FilePath> GetCommandFilePath() {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line && command_line->HasSwitch(kCommandFileFlag)) {
return std::make_optional(
command_line->GetSwitchValuePath(kCommandFileFlag));
}
return std::nullopt;
}
void PrintInstructions(const char* test_file_name) {
const char msg[] = R"(
*** README ***
A captured-site test replays an action recipe (*.test)
against recorded web traffic (*.wpr).
These files need to be downloaded separately.
Recommended flags are:
--test-launcher-timeout=1000000
--ui-test-action-max-timeout=1000000
--enable-pixel-output-in-tests
--vmodule=captured_sites_test_utils=1,%s=1:
For interactive debugging, specify a command file:
--%s=/path/to/file
Commands to step through the test can be written into that file.
Further instructions will be printed then.
)";
VLOG(1) << base::StringPrintf(msg, test_file_name, kCommandFileFlag);
}
// FrameObserver --------------------------------------------------------------
IFrameWaiter::IFrameWaiter(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
query_type_(URL),
target_frame_(nullptr) {}
IFrameWaiter::~IFrameWaiter() {}
content::RenderFrameHost* IFrameWaiter::WaitForFrameMatchingName(
const std::string& name,
const base::TimeDelta timeout) {
content::RenderFrameHost* frame = FrameMatchingPredicateOrNullptr(
web_contents()->GetPrimaryPage(),
base::BindRepeating(&content::FrameMatchesName, name));
if (frame) {
return frame;
} else {
query_type_ = NAME;
frame_name_ = name;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop_.QuitClosure(), timeout);
run_loop_.Run();
return target_frame_;
}
}
content::RenderFrameHost* IFrameWaiter::WaitForFrameMatchingOrigin(
const GURL origin,
const base::TimeDelta timeout) {
content::RenderFrameHost* frame = FrameMatchingPredicateOrNullptr(
web_contents()->GetPrimaryPage(),
base::BindRepeating(&FrameHasOrigin, origin));
if (frame) {
return frame;
} else {
query_type_ = ORIGIN;
origin_ = origin;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop_.QuitClosure(), timeout);
run_loop_.Run();
return target_frame_;
}
}
content::RenderFrameHost* IFrameWaiter::WaitForFrameMatchingUrl(
const GURL url,
const base::TimeDelta timeout) {
content::RenderFrameHost* frame = FrameMatchingPredicateOrNullptr(
web_contents()->GetPrimaryPage(),
base::BindRepeating(&content::FrameHasSourceUrl, url));
if (frame) {
return frame;
} else {
query_type_ = URL;
url_ = url;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop_.QuitClosure(), timeout);
run_loop_.Run();
return target_frame_;
}
}
void IFrameWaiter::RenderFrameCreated(
content::RenderFrameHost* render_frame_host) {
if (!run_loop_.running())
return;
switch (query_type_) {
case NAME:
if (FrameMatchesName(frame_name_, render_frame_host))
run_loop_.Quit();
break;
case ORIGIN:
if (render_frame_host->GetLastCommittedURL().DeprecatedGetOriginAsURL() ==
origin_)
run_loop_.Quit();
break;
case URL:
if (FrameHasSourceUrl(url_, render_frame_host))
run_loop_.Quit();
break;
default:
break;
}
}
void IFrameWaiter::DidFinishLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url) {
if (!run_loop_.running())
return;
switch (query_type_) {
case ORIGIN:
if (validated_url.DeprecatedGetOriginAsURL() == origin_)
run_loop_.Quit();
break;
case URL:
if (FrameHasSourceUrl(validated_url, render_frame_host))
run_loop_.Quit();
break;
default:
break;
}
}
void IFrameWaiter::FrameNameChanged(content::RenderFrameHost* render_frame_host,
const std::string& name) {
if (!run_loop_.running())
return;
switch (query_type_) {
case NAME:
if (FrameMatchesName(name, render_frame_host))
run_loop_.Quit();
break;
default:
break;
}
}
bool IFrameWaiter::FrameHasOrigin(const GURL& origin,
content::RenderFrameHost* frame) {
GURL url = frame->GetLastCommittedURL();
return (url.DeprecatedGetOriginAsURL() == origin.DeprecatedGetOriginAsURL());
}
// WebPageReplayServerWrapper -------------------------------------------------
WebPageReplayServerWrapper::WebPageReplayServerWrapper(
const bool start_as_replay,
int host_http_port,
int host_https_port)
: host_http_port_(host_http_port),
host_https_port_(host_https_port),
start_as_replay_(start_as_replay) {}
WebPageReplayServerWrapper::~WebPageReplayServerWrapper() = default;
bool WebPageReplayServerWrapper::Start(
const base::FilePath& capture_file_path) {
std::vector<std::string> args;
base::FilePath src_dir;
if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &src_dir)) {
ADD_FAILURE() << "Failed to extract the Chromium source directory!";
return false;
}
args.push_back(base::StringPrintf("--http_port=%d", host_http_port_));
args.push_back(base::StringPrintf("--https_port=%d", host_https_port_));
if (start_as_replay_) {
args.push_back("--serve_response_in_chronological_sequence");
// Start WPR in quiet mode, removing the extra verbose ServeHTTP
// interactions that are for the the overwhelming majority unhelpful, but
// for extra debugging of a test case, include 'wpr_verbose' in command.
if (!IsVerboseWprLoggingEnabled())
args.push_back("--quiet_mode");
}
args.push_back(base::StringPrintf(
"--inject_scripts=%s,%s",
FilePathToUTF8(src_dir.AppendASCII("third_party")
.AppendASCII("catapult")
.AppendASCII("web_page_replay_go")
.AppendASCII("deterministic.js")
.value())
.c_str(),
FilePathToUTF8(src_dir.AppendASCII("chrome")
.AppendASCII("test")
.AppendASCII("data")
.AppendASCII("web_page_replay_go_helper_scripts")
.AppendASCII("automation_helper.js")
.value())
.c_str()));
// Specify the capture file.
args.push_back(base::StringPrintf(
"%s", FilePathToUTF8(capture_file_path.value()).c_str()));
if (!RunWebPageReplayCmd(args))
return false;
// Sleep 5 seconds to wait for the web page replay server to start.
// TODO(crbug.com/40578543): create a process std stream reader class to use
// the process output to determine when the server is ready
base::RunLoop wpr_launch_waiter;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, wpr_launch_waiter.QuitClosure(), base::Seconds(5));
wpr_launch_waiter.Run();
if (!web_page_replay_server_.IsValid()) {
ADD_FAILURE() << "Failed to start the WPR replay server!";
return false;
}
return true;
}
bool WebPageReplayServerWrapper::Stop() {
if (web_page_replay_server_.IsValid()) {
bool did_terminate = false;
if (!start_as_replay_) {
#if BUILDFLAG(IS_POSIX)
// For Replay sessions, we can terminate the WPR process immediately as
// we don't Record sessions, we want to try and send a SIGINT to close and
// write the WPR archive file gracefully. If that fails, we will Terminate
// via Process::Terminate which will send SIGTERM and then SIGKILL.
did_terminate = kill(web_page_replay_server_.Handle(), SIGINT) == 0;
if (!did_terminate) {
ADD_FAILURE() << "Failed to close a recording WPR server cleanly!";
}
#else
ADD_FAILURE()
<< "Clean termination of recrording WPR server is only supported on "
"OS_POSIX. New archive may not be saved properly.";
#endif
}
if (start_as_replay_ || !did_terminate) {
if (!web_page_replay_server_.Terminate(0, true)) {
ADD_FAILURE() << "Failed to terminate the WPR replay server!";
return false;
}
}
}
// The test server hasn't started, no op.
return true;
}
bool WebPageReplayServerWrapper::RunWebPageReplayCmdAndWaitForExit(
const std::vector<std::string>& args,
const base::TimeDelta& timeout) {
int exit_code;
if (RunWebPageReplayCmd(args) && web_page_replay_server_.IsValid() &&
web_page_replay_server_.WaitForExitWithTimeout(timeout, &exit_code) &&
exit_code == 0) {
return true;
}
ADD_FAILURE() << "Failed to run WPR command: '" << cmd_name() << "'!";
return false;
}
bool WebPageReplayServerWrapper::RunWebPageReplayCmd(
const std::vector<std::string>& args) {
// Allow the function to block. Otherwise the subsequent call to
// base::PathExists will fail. base::PathExists must be called from
// a scope that allows blocking.
base::ScopedAllowBlockingForTesting allow_blocking;
base::LaunchOptions options = base::LaunchOptionsForTest();
base::FilePath exe_dir;
if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &exe_dir)) {
ADD_FAILURE() << "Failed to extract the Chromium source directory!";
return false;
}
base::FilePath web_page_replay_binary_dir = exe_dir.AppendASCII("third_party")
.AppendASCII("catapult")
.AppendASCII("telemetry")
.AppendASCII("telemetry")
.AppendASCII("bin");
options.current_directory = web_page_replay_binary_dir;
#if BUILDFLAG(IS_WIN)
base::FilePath wpr_executable_binary =
base::FilePath(FILE_PATH_LITERAL("win"))
.AppendASCII("AMD64")
.AppendASCII("wpr.exe");
#elif BUILDFLAG(IS_MAC)
base::FilePath wpr_executable_binary =
base::FilePath(FILE_PATH_LITERAL("mac"))
.AppendASCII("x86_64")
.AppendASCII("wpr");
#elif BUILDFLAG(IS_POSIX)
base::FilePath wpr_executable_binary =
base::FilePath(FILE_PATH_LITERAL("linux"))
.AppendASCII("x86_64")
.AppendASCII("wpr");
#else
#error Platform is not supported.
#endif
base::CommandLine full_command(
web_page_replay_binary_dir.Append(wpr_executable_binary));
full_command.AppendArg(cmd_name());
// Ask web page replay to use the custom certificate and key files used to
// make the web page captures.
// The capture files used in these browser tests are also used on iOS to
// test autofill.
// The custom cert and key files are different from those of the official
// WPR releases. The custom files are made to work on iOS.
base::FilePath src_dir;
if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &src_dir)) {
ADD_FAILURE() << "Failed to extract the Chromium source directory!";
return false;
}
base::FilePath web_page_replay_support_file_dir =
src_dir.AppendASCII("components")
.AppendASCII("test")
.AppendASCII("data")
.AppendASCII("autofill")
.AppendASCII("web_page_replay_support_files");
full_command.AppendArg(base::StringPrintf(
"--https_cert_file=%s,%s",
FilePathToUTF8(
web_page_replay_support_file_dir.AppendASCII("wpr_cert.pem").value())
.c_str(),
FilePathToUTF8(
web_page_replay_support_file_dir.AppendASCII("ecdsa_cert.pem")
.value())
.c_str()));
full_command.AppendArg(base::StringPrintf(
"--https_key_file=%s,%s",
FilePathToUTF8(
web_page_replay_support_file_dir.AppendASCII("wpr_key.pem").value())
.c_str(),
FilePathToUTF8(
web_page_replay_support_file_dir.AppendASCII("ecdsa_key.pem").value())
.c_str()));
for (const auto& arg : args)
full_command.AppendArg(arg);
VLOG(1) << full_command.GetArgumentsString();
web_page_replay_server_ = base::LaunchProcess(full_command, options);
return true;
}
// ProfileDataController ------------------------------------------------------
ProfileDataController::ProfileDataController()
: profile_(autofill::test::GetIncompleteProfile2()),
card_(autofill::CreditCard(
base::Uuid::GenerateRandomV4().AsLowercaseString(),
"http://www.example.com")) {
// Initialize the credit card with default values, in case the test recipe
// file does not contain pre-saved credit card info.
autofill::test::SetCreditCardInfo(&card_, "Buddy Holly", "5187654321098765",
"10", "2998", "1");
}
ProfileDataController::~ProfileDataController() = default;
bool ProfileDataController::AddAutofillProfileInfo(
const std::string& field_type,
const std::string& field_value) {
std::optional<autofill::FieldType> type = StringToFieldType(field_type);
if (!type.has_value()) {
ADD_FAILURE() << "Unable to recognize autofill field type '" << field_type
<< "'!";
return false;
}
if (base::StartsWith(field_type, "HTML_TYPE_CREDIT_CARD_",
base::CompareCase::INSENSITIVE_ASCII) ||
base::StartsWith(field_type, "CREDIT_CARD_",
base::CompareCase::INSENSITIVE_ASCII)) {
if (type == autofill::CREDIT_CARD_VERIFICATION_CODE) {
cvc_ = base::UTF8ToUTF16(field_value);
}
card_.SetRawInfo(type.value(), base::UTF8ToUTF16(field_value));
} else {
profile_.SetRawInfo(type.value(), base::UTF8ToUTF16(field_value));
}
return true;
}
// TestRecipeReplayer ---------------------------------------------------------
TestRecipeReplayer::TestRecipeReplayer(
Browser* browser,
TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor)
: browser_(browser), feature_action_executor_(feature_action_executor) {
CleanupSiteData();
// Bypass permission dialogs.
permissions::PermissionRequestManager::FromWebContents(GetWebContents())
->set_auto_response_for_test(
permissions::PermissionRequestManager::ACCEPT_ALL);
}
TestRecipeReplayer::~TestRecipeReplayer() {
// If there are still cookies at the time the browser test shuts down,
// Chrome's SQL lite persistent cookie store will crash.
CleanupSiteData();
EXPECT_TRUE(web_page_replay_server_wrapper()->Stop())
<< "Cannot stop the local Web Page Replay server.";
}
bool TestRecipeReplayer::ReplayTest(
const base::FilePath& capture_file_path,
const base::FilePath& recipe_file_path,
const std::optional<base::FilePath>& command_file_path) {
logging::SetMinLogLevel(logging::LOGGING_WARNING);
if (!web_page_replay_server_wrapper()->Start(capture_file_path))
return false;
if (OverrideAutofillClock(capture_file_path))
VLOG(1) << "AutofillClock was set to:" << autofill::AutofillClock::Now();
return ReplayRecordedActions(recipe_file_path, command_file_path);
}
const std::vector<testing::AssertionResult>
TestRecipeReplayer::GetValidationFailures() const {
return validation_failures_;
}
// Extracts the time of the wpr recording from the wpr archive file and
// overrides the autofill::AutofillClock to match that time.
bool TestRecipeReplayer::OverrideAutofillClock(
const base::FilePath capture_file_path) {
std::string json_text;
{
base::ScopedAllowBlockingForTesting allow_blocking;
if (!base::ReadFileToString(capture_file_path, &json_text)) {
VLOG(1) << kClockNotSetMessage << "Could not read file";
return false;
}
}
// Decompress the json text from gzip.
std::string decompressed_json_text;
if (!compression::GzipUncompress(json_text, &decompressed_json_text)) {
VLOG(1) << kClockNotSetMessage << "Could not gzip decompress file";
return false;
}
// Convert the file text into a json object.
std::optional<base::Value> parsed_json =
base::JSONReader::Read(decompressed_json_text);
if (!parsed_json) {
VLOG(1) << kClockNotSetMessage << "Failed to deserialize json";
return false;
}
const std::optional<double> time_value =
parsed_json->GetDict().FindDouble("DeterministicTimeSeedMs");
if (!time_value) {
VLOG(1) << kClockNotSetMessage << "No DeterministicTimeSeedMs found";
return false;
}
test_clock_.SetNow(base::Time::FromMillisecondsSinceUnixEpoch(*time_value));
return true;
}
// static
void TestRecipeReplayer::SetUpHostResolverRules(
base::CommandLine* command_line) {
// Direct traffic to the Web Page Replay server.
command_line->AppendSwitchASCII(
network::switches::kHostResolverRules,
base::StringPrintf(
"MAP *:80 127.0.0.1:%d,"
"MAP *:443 127.0.0.1:%d,"
// Set to always exclude, allows cache_replayer overwrite
"EXCLUDE clients1.google.com,"
"EXCLUDE content-autofill.googleapis.com,"
"EXCLUDE localhost",
kHostHttpPort, kHostHttpsPort));
}
// static
void TestRecipeReplayer::SetUpCommandLine(base::CommandLine* command_line) {
command_line->AppendSwitchASCII(
network::switches::kIgnoreCertificateErrorsSPKIList,
kWebPageReplayCertSPKI);
command_line->AppendSwitch(switches::kStartMaximized);
// Since we are adding via ScopedFeatureList for test features required, we
// need to explicitly also enable field trials.
command_line->AppendSwitch(
variations::switches::kEnableFieldTrialTestingConfig);
}
TestRecipeReplayChromeFeatureActionExecutor*
TestRecipeReplayer::feature_action_executor() {
return feature_action_executor_;
}
Browser* TestRecipeReplayer::browser() {
return browser_;
}
WebPageReplayServerWrapper*
TestRecipeReplayer::web_page_replay_server_wrapper() {
return web_page_replay_server_wrapper_.get();
}
content::WebContents* TestRecipeReplayer::GetWebContents() {
return browser_->tab_strip_model()->GetActiveWebContents();
}
void TestRecipeReplayer::WaitTillPageIsIdle(
base::TimeDelta continuous_paint_timeout) {
// Loop continually while WebContents are waiting for response or loading.
// page_is_loading is expectedWaitTillPageIsIdle to always got to False
// eventually, but adding a timeout as a fallback.
base::TimeTicks finished_load_time = base::TimeTicks::Now();
while (true) {
{
base::RunLoop heart_beat;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, heart_beat.QuitClosure(), wait_for_idle_loop_length);
heart_beat.Run();
}
bool page_is_loading = GetWebContents()->IsWaitingForResponse() ||
GetWebContents()->IsLoading();
if (!page_is_loading)
break;
if ((base::TimeTicks::Now() - finished_load_time) >
continuous_paint_timeout) {
VLOG(1) << "Page is still loading after "
<< visual_update_timeout.InSeconds()
<< " seconds. Bailing because timeout was reached.";
break;
}
}
finished_load_time = base::TimeTicks::Now();
while (true) {
// Now, rely on the render frame count to be the indicator of page activity.
// Once all the frames are drawn, we're free to continue.
content::RenderFrameSubmissionObserver frame_submission_observer(
GetWebContents());
{
base::RunLoop heart_beat;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, heart_beat.QuitClosure(), wait_for_idle_loop_length);
heart_beat.Run();
}
if (frame_submission_observer.render_frame_count() == 0) {
// If the render r has stopped submitting frames
break;
} else if ((base::TimeTicks::Now() - finished_load_time) >
continuous_paint_timeout) {
// |continuous_paint_timeout| has expired since Chrome loaded the page.
// During this period of time, Chrome has been continuously painting
// the page. In this case, the page is probably idle, but a bug, a
// blinking caret or a persistent animation is keeping the
// |render_frame_count| from reaching zero. Exit.
VLOG(1) << "Wait for render frame count timed out after "
<< continuous_paint_timeout.InSeconds()
<< " seconds with the frame count still at: "
<< frame_submission_observer.render_frame_count();
break;
}
}
}
bool TestRecipeReplayer::WaitForVisualUpdate(base::TimeDelta timeout) {
base::TimeTicks start_time = base::TimeTicks::Now();
content::RenderFrameSubmissionObserver frame_submission_observer(
GetWebContents());
while (frame_submission_observer.render_frame_count() == 0) {
base::RunLoop heart_beat;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, heart_beat.QuitClosure(), wait_for_idle_loop_length);
heart_beat.Run();
if ((base::TimeTicks::Now() - start_time) > timeout) {
return false;
}
}
return true;
}
void TestRecipeReplayer::CleanupSiteData() {
// Navigate to about:blank, then clear the browser cache.
// Navigating to about:blank before clearing the cache ensures that
// the cleanup is thorough and nothing is held.
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser_, GURL(url::kAboutBlankURL)));
content::BrowsingDataRemover* remover =
browser_->profile()->GetBrowsingDataRemover();
content::BrowsingDataRemoverCompletionObserver completion_observer(remover);
remover->RemoveAndReply(
base::Time(), base::Time::Max(),
content::BrowsingDataRemover::DATA_TYPE_COOKIES,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB,
&completion_observer);
completion_observer.BlockUntilCompletion();
}
bool TestRecipeReplayer::ReplayRecordedActions(
const base::FilePath& recipe_file_path,
const std::optional<base::FilePath>& command_file_path) {
// Read the text of the recipe file.
base::ScopedAllowBlockingForTesting for_testing;
std::string json_text;
if (!base::ReadFileToString(recipe_file_path, &json_text)) {
ADD_FAILURE() << "Failed to read recipe file '" << recipe_file_path << "'!";
return false;
}
// Convert the file text into a json object.
std::optional<base::Value> parsed_json = base::JSONReader::Read(json_text);
if (!parsed_json) {
ADD_FAILURE() << "Failed to deserialize json text!";
return false;
}
DCHECK(parsed_json->is_dict());
base::Value::Dict recipe = std::move(*parsed_json).TakeDict();
if (!InitializeBrowserToExecuteRecipe(recipe))
return false;
// Iterate through and execute each action in the recipe.
base::Value::List* action_list = recipe.FindList("actions");
if (!action_list) {
ADD_FAILURE() << "Failed to extract action list from the recipe!";
return false;
}
ExecutionState execution_state{.length =
static_cast<int>(action_list->size())};
if (command_file_path.has_value()) {
execution_state.limit = 0; // Stop execution initially in debug mode.
PrintDebugInstructions(command_file_path.value());
}
while (execution_state.index < execution_state.length) {
if (execution_state.pause_on_failure &&
(testing::Test::HasNonfatalFailure() ||
testing::Test::HasFatalFailure() || validation_failures_.size() > 0)) {
// If set to pause on a failure, move limit to current, but then reset
// `pause_on_failure` so it can continue if the user requests.
execution_state.limit = execution_state.index;
execution_state.pause_on_failure = false;
}
if (command_file_path.has_value()) {
while (execution_state.limit <= execution_state.index) {
bool thread_finished = false;
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&ProcessCommands, execution_state, action_list,
command_file_path.value()),
base::BindOnce(
[](ExecutionState* execution_state, bool* finished,
ExecutionState new_execution_state) {
*execution_state = new_execution_state;
*finished = true;
},
&execution_state, &thread_finished));
while (!thread_finished) {
base::RunLoop run_loop;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), base::Seconds(1));
run_loop.Run();
}
}
}
VLOG(1) << "Proceeding with execution with action " << execution_state.index
<< " of " << execution_state.length << ": "
<< (*action_list)[execution_state.index];
if (!(*action_list)[execution_state.index].is_dict()) {
ADD_FAILURE()
<< "Failed to extract an individual action from the recipe!";
return false;
}
base::Value::Dict action =
std::move((*action_list)[execution_state.index].GetDict());
std::optional<std::string> type =
FindPopulateString(action, "type", "action type");
if (!type)
return false;
if (base::CompareCaseInsensitiveASCII(*type, "autofill") == 0) {
if (!ExecuteAutofillAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "click") == 0) {
if (!ExecuteClickAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "clickIfNotSeen") ==
0) {
if (!ExecuteClickIfNotSeenAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "closeTab") == 0) {
if (!ExecuteCloseTabAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "coolOff") == 0) {
if (!ExecuteCoolOffAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "executeScript") == 0) {
if (!ExecuteRunCommandAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "hover") == 0) {
if (!ExecuteHoverAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "loadPage") == 0) {
if (!ExecuteForceLoadPage(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "pressEnter") == 0) {
if (!ExecutePressEnterAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "pressEscape") == 0) {
if (!ExecutePressEscapeAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "pressSpace") == 0) {
if (!ExecutePressSpaceAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "savePassword") == 0) {
if (!ExecuteSavePasswordAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "select") == 0) {
if (!ExecuteSelectDropdownAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "type") == 0) {
if (!ExecuteTypeAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "typePassword") == 0) {
if (!ExecuteTypePasswordAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "updatePassword") ==
0) {
if (!ExecuteUpdatePasswordAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "validateField") == 0) {
if (!ExecuteValidateFieldValueAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(
*type, "validatePasswordGenerationPrompt") == 0) {
if (!ExecuteValidatePasswordGenerationPromptAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(
*type, "validateNoSavePasswordPrompt") == 0) {
if (!ExecuteValidateNoSavePasswordPromptAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(
*type, "validatePasswordSaveFallback") == 0) {
if (!ExecuteValidateSaveFallbackAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "waitFor") == 0) {
if (!ExecuteWaitForStateAction(std::move(action)))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "breakpoint") == 0) {
execution_state.limit = execution_state.index + 1;
} else {
ADD_FAILURE() << "Unrecognized action type: " << *type;
}
++execution_state.index;
}
return true;
}
// Functions for deserializing and executing actions from the test recipe
// JSON object.
bool TestRecipeReplayer::InitializeBrowserToExecuteRecipe(
base::Value::Dict& recipe) {
// Setup any saved address and credit card at the start of the test.
auto* autofill_profile_container = recipe.Find("autofillProfile");
if (autofill_profile_container) {
if (!autofill_profile_container->is_list()) {
ADD_FAILURE() << "Save Autofill Profile is not a list!";
return false;
}
if (!SetupSavedAutofillProfile(
std::move(*autofill_profile_container).TakeList())) {
return false;
}
}
// Setup any saved passwords at the start of the test.
auto* saved_password_container = recipe.Find("passwordManagerProfiles");
if (saved_password_container) {
if (!saved_password_container->is_list()) {
ADD_FAILURE() << "Saved Password List is not a list!";
return false;
}
if (!SetupSavedPasswords(std::move(*saved_password_container).TakeList())) {
return false;
}
}
// Extract the starting URL from the test recipe.
auto* starting_url = recipe.FindString("startingURL");
if (!starting_url) {
ADD_FAILURE() << "Failed to extract the starting url from the recipe!";
return false;
}
// Navigate to the starting URL, wait for the page to complete loading.
if (!content::ExecJs(GetWebContents(),
base::StringPrintf("window.location.href = '%s';",
starting_url->c_str()))) {
ADD_FAILURE() << "Failed to navigate Chrome to '" << *starting_url << "!";
return false;
}
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteAutofillAction(base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
return false;
std::vector<std::string> frame_path;
if (!GetIFramePathFromAction(action, &frame_path))
return false;
VLOG(1) << "Invoking Chrome Autofill on `" << xpath << "`.";
// Clear the input box first, in case a previous value is there.
// If the text input box is not clear, pressing the down key will not
// bring up the autofill suggestion box.
// This can happen on sites that requires the user to sign in. After
// signing in, the site fills the form with the user's profile
// information.
if (!ExecuteJavaScriptOnElementByXpath(
frame, xpath,
"automation_helper.setInputElementValue(target, ``);")) {
ADD_FAILURE() << "Failed to clear the input field value!";
return false;
}
std::string autofill_triggered_field_type;
if (GetElementProperty(frame, xpath,
"return target.getAttribute('autofill-prediction');",
&autofill_triggered_field_type)) {
VLOG(1) << "The field's Chrome Autofill annotation: "
<< autofill_triggered_field_type << " during autofill form step.";
} else {
VLOG(1) << "Failed to obtain the field's Chrome Autofill annotation during "
"autofill form step!";
}
if (!feature_action_executor()->AutofillForm(
xpath, frame_path, kAutofillActionNumRetries, frame,
StringToFieldType(autofill_triggered_field_type))) {
return false;
}
WaitTillPageIsIdle(kAutofillActionWaitForVisualUpdateTimeout);
return true;
}
bool TestRecipeReplayer::ExecuteClickAction(base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
return false;
VLOG(1) << "Left mouse clicking `" << xpath << "`.";
if (!ScrollElementIntoView(xpath, frame))
return false;
WaitTillPageIsIdle(scroll_wait_timeout);
gfx::Rect rect;
if (!GetBoundingRectOfTargetElement(xpath, frame, &rect))
return false;
if (!SimulateLeftMouseClickAt(rect.CenterPoint(), frame))
return false;
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteClickIfNotSeenAction(base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (ExtractFrameAndVerifyElement(action, &xpath, &frame, false, false,
true)) {
return true;
} else {
// If the selector wasn't found, take the clickSelector and make it the
// selector to attempt a click with that element instead.
std::optional<std::string> click_xpath_text =
FindPopulateString(action, "clickSelector", "click xpath selector");
action.Set("selector", *click_xpath_text);
return ExecuteClickAction(std::move(action));
}
}
bool TestRecipeReplayer::ExecuteCloseTabAction(base::Value::Dict action) {
VLOG(1) << "Closing Active Tab";
browser_->tab_strip_model()->CloseSelectedTabs();
return true;
}
bool TestRecipeReplayer::ExecuteCoolOffAction(base::Value::Dict action) {
base::RunLoop heart_beat;
base::TimeDelta cool_off_time = cool_off_action_timeout;
base::Value* pause_time_container = action.Find("pauseTimeSec");
if (pause_time_container) {
if (!pause_time_container->is_int()) {
ADD_FAILURE() << "Pause time is not an integer!";
return false;
}
int seconds = pause_time_container->GetInt();
cool_off_time = base::Seconds(seconds);
}
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, heart_beat.QuitClosure(), cool_off_time);
VLOG(1) << "Pausing execution for '" << cool_off_time.InSeconds()
<< "' seconds";
heart_beat.Run();
return true;
}
bool TestRecipeReplayer::ExecuteHoverAction(base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
return false;
VLOG(1) << "Hovering over `" << xpath << "`.";
if (!ScrollElementIntoView(xpath, frame))
return false;
WaitTillPageIsIdle(scroll_wait_timeout);
gfx::Rect rect;
if (!GetBoundingRectOfTargetElement(xpath, frame, &rect))
return false;
if (!SimulateMouseHoverAt(frame, rect.CenterPoint()))
return false;
if (!WaitForVisualUpdate()) {
ADD_FAILURE() << "The page did not respond to a mouse hover action!";
return false;
}
return true;
}
bool TestRecipeReplayer::ExecuteForceLoadPage(base::Value::Dict action) {
bool should_force = action.FindBool("force").value_or(false);
if (!should_force) {
return true;
}
std::optional<std::string> url =
FindPopulateString(action, "url", "Force Load URL");
if (!url)
return false;
VLOG(1) << "Making explicit URL redirect to '" << *url << "'";
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser_, GURL(*url)));
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecutePressEnterAction(base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
return false;
VLOG(1) << "Pressing 'Enter' on `" << xpath << "`.";
SimulateKeyPressWrapper(content::WebContents::FromRenderFrameHost(frame),
ui::DomKey::ENTER);
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecutePressEscapeAction(base::Value::Dict action) {
VLOG(1) << "Pressing 'Esc' in the current frame";
SimulateKeyPressWrapper(GetWebContents(), ui::DomKey::ESCAPE);
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecutePressSpaceAction(base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, true))
return false;
VLOG(1) << "Pressing 'Space' on `" << xpath << "`.";
SimulateKeyPressWrapper(content::WebContents::FromRenderFrameHost(frame),
ui::DomKey::FromCharacter(' '));
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteRunCommandAction(base::Value::Dict action) {
// Extract the list of JavaScript commands into a vector.
std::vector<std::string> commands;
base::Value::List* list = action.FindList("commands");
if (!list) {
ADD_FAILURE() << "Failed to extract commands list from action";
return false;
}
for (const auto& command : *list) {
if (!command.is_string()) {
ADD_FAILURE() << "command is not a string: " << command;
return false;
}
commands.push_back(command.GetString());
}
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame)) {
return false;
}
VLOG(1) << "Running JavaScript commands on the page.";
// Execute the commands.
for (const std::string& command : commands) {
if (!content::ExecJs(frame, command)) {
ADD_FAILURE() << "Failed to execute JavaScript command `" << command
<< "`!";
return false;
}
// Wait in case the JavaScript command triggers page load or layout
// changes.
WaitTillPageIsIdle();
}
return true;
}
bool TestRecipeReplayer::ExecuteSavePasswordAction(base::Value::Dict action) {
VLOG(1) << "Save password.";
if (!feature_action_executor()->SavePassword())
return false;
bool stored_cred;
if (!HasChromeStoredCredential(action, &stored_cred))
return false;
if (!stored_cred) {
ADD_FAILURE() << "Chrome did not save the credential!";
return false;
}
return true;
}
bool TestRecipeReplayer::ExecuteSelectDropdownAction(base::Value::Dict action) {
std::optional<int> index = action.FindInt("index");
if (!index.has_value()) {
ADD_FAILURE() << "Failed to extract Selection Index from action";
return false;
}
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
return false;
VLOG(1) << "Select option '" << *index << "' from `" << xpath << "`.";
if (!ExecuteJavaScriptOnElementByXpath(
frame, xpath,
base::StringPrintf(
"automation_helper"
" .selectOptionFromDropDownElementByIndex(target, %d);",
*index))) {
ADD_FAILURE() << "Failed to select drop down option with JavaScript!";
return false;
}
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteTypeAction(base::Value::Dict action) {
std::optional<std::string> value =
FindPopulateString(action, "value", "typing value");
if (!value)
return false;
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
return false;
VLOG(1) << "Typing '" << *value << "' inside `" << xpath << "`.";
if (!ExecuteJavaScriptOnElementByXpath(
frame, xpath,
base::StringPrintf(
"automation_helper.setInputElementValue(target, `%s`);",
value->c_str()))) {
ADD_FAILURE() << "Failed to type inside input element with JavaScript!";
return false;
}
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteTypePasswordAction(base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, true))
return false;
std::optional<std::string> value =
FindPopulateString(action, "value", "password text");
if (!value)
return false;
// Clear the password field first, in case a previous value is there.
if (!ExecuteJavaScriptOnElementByXpath(
frame, xpath,
"automation_helper.setInputElementValue(target, ``);")) {
ADD_FAILURE() << "Failed to execute JavaScript to clear the input value!";
return false;
}
VLOG(1) << "Typing '" << *value << "' inside `" << xpath << "`.";
content::WebContents* web_contents =
content::WebContents::FromRenderFrameHost(frame);
for (size_t index = 0; index < value->size(); index++) {
SimulateKeyPressWrapper(web_contents,
ui::DomKey::FromCharacter(value->at(index)));
}
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteUpdatePasswordAction(base::Value::Dict action) {
VLOG(1) << "Update password.";
if (!feature_action_executor()->UpdatePassword())
return false;
bool stored_cred;
if (!HasChromeStoredCredential(action, &stored_cred))
return false;
if (!stored_cred) {
ADD_FAILURE() << "Chrome did not update the credential!";
return false;
}
return true;
}
bool TestRecipeReplayer::ExecuteValidateFieldValueAction(
base::Value::Dict action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, false, true))
return false;
base::Value* autofill_prediction_container =
action.Find("expectedAutofillType");
if (autofill_prediction_container) {
if (!autofill_prediction_container->is_string()) {
ADD_FAILURE() << "Autofill prediction is not a string!";
return false;
}
// If we are validating the value of a Chrome autofilled field, print the
// Chrome Autofill's field annotation for debugging purpose.
std::string autofill_information;
if (GetElementProperty(
frame, xpath, "return target.getAttribute('autofill-information');",
&autofill_information)) {
VLOG(1) << autofill_information;
} else {
// Only used for logging purposes, so don't ADD_FAILURE() if it fails.
VLOG(1) << "Failed to obtain the field's Chrome Autofill annotation!";
}
std::string expected_autofill_prediction_type =
autofill_prediction_container->GetString();
VLOG(1) << "Checking the field `" << xpath << "` has the autofill type '"
<< expected_autofill_prediction_type << "'";
ExpectElementPropertyEqualsAnyOf(
frame, xpath, "return target.getAttribute('autofill-prediction');",
{expected_autofill_prediction_type}, "autofill type mismatch",
IgnoreCase(true));
}
std::optional<std::vector<std::string>> expected_values =
FindPopulateStringVector(action, "expectedValues", AllowNull());
std::optional<std::string> expected_value =
FindPopulateString(action, "expectedValue", AllowNull());
if (!!expected_values == !!expected_value) {
ADD_FAILURE() << "Failed to extract 'expectedValue' xor 'expectedValues' "
"vector from container !";
return false;
}
VLOG(1) << "Checking the field `" << xpath << "`.";
if (expected_value) {
ExpectElementPropertyEqualsAnyOf(frame, xpath, "return target.value;",
{*expected_value}, "text value mismatch");
}
if (expected_values) {
ExpectElementPropertyEqualsAnyOf(frame, xpath, "return target.value;",
*expected_values, "text value mismatch");
}
return true;
}
bool TestRecipeReplayer::ExecuteValidateNoSavePasswordPromptAction(
base::Value::Dict action) {
VLOG(1) << "Verify that the page hasn't shown a save password prompt.";
EXPECT_FALSE(feature_action_executor()->HasChromeShownSavePasswordPrompt());
return true;
}
bool TestRecipeReplayer::ExecuteValidatePasswordGenerationPromptAction(
base::Value::Dict action) {
VLOG(1) << "Verify that an element is properly displaying or not displaying "
"the password generation prompt";
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, true))
return false;
// Most common scenario is validating that the password generation prompt is
// being shown, so if unspecified default to true.
bool expect_to_be_shown = expect_to_be_shown =
action.FindBool("shouldBeShown").value_or(true);
// First, execute a click to focus on the field in question.
ExecuteClickAction(std::move(action));
// Validate that the password generation prompt is shown when expected.
ValidatePasswordGenerationPromptState(frame, xpath, expect_to_be_shown);
return true;
}
void TestRecipeReplayer::ValidatePasswordGenerationPromptState(
const content::ToRenderFrameHost& frame,
const std::string& element_xpath,
bool expect_to_be_shown) {
bool is_password_generation_prompt_showing =
feature_action_executor()->IsChromeShowingPasswordGenerationPrompt();
if (expect_to_be_shown != is_password_generation_prompt_showing) {
std::string error_message =
base::StrCat({"Field xpath: `", element_xpath, "` is",
is_password_generation_prompt_showing ? "" : " not",
" showing password generation prompt but it should",
expect_to_be_shown ? "" : " not", " be."});
VLOG(1) << error_message;
ADD_FAILURE() << error_message;
validation_failures_.push_back(testing::AssertionFailure()
<< error_message);
}
}
bool TestRecipeReplayer::ExecuteValidateSaveFallbackAction(
base::Value::Dict action) {
VLOG(1) << "Verify that Chrome shows the save fallback icon in the omnibox.";
EXPECT_TRUE(feature_action_executor()->WaitForSaveFallback());
return true;
}
bool TestRecipeReplayer::ExecuteWaitForStateAction(base::Value::Dict action) {
// Extract the list of JavaScript assertions into a vector.
std::vector<std::string> state_assertions;
base::Value::List* list = action.FindList("assertions");
if (!list) {
ADD_FAILURE() << "Failed to extract wait assertions list from action";
return false;
}
for (const base::Value& assertion : *list) {
if (!assertion.is_string()) {
ADD_FAILURE() << "Assertion is not a string: " << assertion;
return false;
}
state_assertions.push_back(assertion.GetString());
}
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
VLOG(1) << "Waiting for page to reach a state.";
// Wait for all of the assertions to become true on the current page.
return WaitForStateChange(frame, state_assertions, default_action_timeout);
}
bool TestRecipeReplayer::GetTargetHTMLElementXpathFromAction(
const base::Value::Dict& action,
std::string* xpath) {
xpath->clear();
std::optional<std::string> xpath_text =
FindPopulateString(action, "selector", "xpath selector");
if (!xpath_text)
return false;
*xpath = *xpath_text;
return true;
}
bool TestRecipeReplayer::GetTargetHTMLElementVisibilityEnumFromAction(
const base::Value::Dict& action,
int* visibility_enum_val) {
const base::Value* visibility_container = action.Find("visibility");
if (!visibility_container) {
// By default, set the visibility to (visible | enabled | on_top), as
// defined in
// chrome/test/data/web_page_replay_go_helper_scripts/automation_helper.js
*visibility_enum_val = 7;
return true;
}
if (!visibility_container->is_int()) {
ADD_FAILURE() << "visibility property is not an integer!";
return false;
}
*visibility_enum_val = visibility_container->GetInt();
return true;
}
bool TestRecipeReplayer::GetTargetFrameFromAction(
const base::Value::Dict& action,
content::RenderFrameHost** frame) {
const base::Value* iframe_container = action.Find("context");
if (!iframe_container) {
ADD_FAILURE() << "Failed to extract the iframe context from action!";
return false;
}
if (!iframe_container->is_dict()) {
ADD_FAILURE() << "Failed to extract the iframe context object!";
return false;
}
std::optional<bool> is_iframe_container =
iframe_container->GetDict().FindBool("isIframe");
if (!is_iframe_container) {
ADD_FAILURE() << "Failed to extract isIframe from the iframe context! ";
return false;
}
if (!is_iframe_container.value_or(false)) {
*frame = GetWebContents()->GetPrimaryMainFrame();
return true;
}
const base::Value* frame_name_container =
iframe_container->GetDict().FindByDottedPath("browserTest.name");
const base::Value* frame_origin_container =
iframe_container->GetDict().FindByDottedPath("browserTest.origin");
const base::Value* frame_url_container =
iframe_container->GetDict().FindByDottedPath("browserTest.url");
IFrameWaiter iframe_waiter(GetWebContents());
if (frame_name_container != nullptr && !frame_name_container->is_string()) {
ADD_FAILURE() << "Iframe name is not a string!";
return false;
}
if (frame_origin_container != nullptr &&
!frame_origin_container->is_string()) {
ADD_FAILURE() << "Iframe origin is not a string!";
return false;
}
if (frame_url_container != nullptr && !frame_url_container->is_string()) {
ADD_FAILURE() << "Iframe url is not a string!";
return false;
}
if (frame_name_container != nullptr) {
std::string frame_name = frame_name_container->GetString();
*frame = iframe_waiter.WaitForFrameMatchingName(frame_name);
} else if (frame_origin_container != nullptr) {
std::string frame_origin = frame_origin_container->GetString();
*frame = iframe_waiter.WaitForFrameMatchingOrigin(GURL(frame_origin));
} else if (frame_url_container != nullptr) {
std::string frame_url = frame_url_container->GetString();
*frame = iframe_waiter.WaitForFrameMatchingUrl(GURL(frame_url));
} else {
ADD_FAILURE() << "The recipe does not specify a way to find the iframe!";
}
if (*frame == nullptr) {
ADD_FAILURE() << "Failed to find iframe!";
return false;
}
return true;
}
bool TestRecipeReplayer::ExtractFrameAndVerifyElement(
const base::Value::Dict& action,
std::string* xpath,
content::RenderFrameHost** frame,
bool set_focus,
bool relaxed_visibility,
bool ignore_failure) {
if (!GetTargetHTMLElementXpathFromAction(action, xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
if (!GetTargetFrameFromAction(action, frame))
return false;
// If we're just validating we don't care about on_top-ness, as copied from
// chrome/test/data/web_page_replay_go_helper_scripts/automation_helper.js
// to TestRecipeReplayer::DomElementReadyState enum
// So remove (DomElementReadyState::kReadyStateOnTop)
if (relaxed_visibility)
visibility_enum_val &= ~kReadyStateOnTop;
if (!WaitForElementToBeReady(*xpath, visibility_enum_val, *frame,
ignore_failure))
return false;
if (set_focus) {
std::vector<std::string> frame_path;
if (!GetIFramePathFromAction(action, &frame_path))
return false;
if (!PlaceFocusOnElement(*xpath, frame_path, *frame))
return false;
}
return true;
}
bool TestRecipeReplayer::GetIFramePathFromAction(
const base::Value::Dict& action,
std::vector<std::string>* iframe_path) {
*iframe_path = std::vector<std::string>();
const base::Value* iframe_container = action.Find("context");
if (!iframe_container) {
ADD_FAILURE() << "Failed to extract the iframe context from action!";
return false;
}
if (!iframe_container->is_dict()) {
ADD_FAILURE() << "Failed to extract the iframe context object!";
return false;
}
const base::Value* iframe_path_container =
iframe_container->GetDict().Find("path");
if (!iframe_path_container) {
// If the action does not have a path container, it would mean that:
// 1. The target frame is the top level frame.
// 2. The target frame is an iframe, but it is the top-level frame in its
// rendering process.
return true;
}
if (!iframe_path_container->is_list()) {
ADD_FAILURE() << "The action's iframe path is not a list!";
return false;
}
for (const auto& xpath : iframe_path_container->GetList()) {
if (!xpath.is_string()) {
ADD_FAILURE() << "Failed to extract the iframe xpath from action!";
return false;
}
iframe_path->push_back(xpath.GetString());
}
return true;
}
bool TestRecipeReplayer::GetIFrameOffsetFromIFramePath(
const std::vector<std::string>& iframe_path,
content::RenderFrameHost* frame,
gfx::Vector2d* offset) {
*offset = gfx::Vector2d(0, 0);
for (auto it_xpath = iframe_path.begin(); it_xpath != iframe_path.end();
it_xpath++) {
content::RenderFrameHost* parent_frame = frame->GetParent();
if (parent_frame == nullptr) {
ADD_FAILURE() << "Trying to iterate past the top level frame!";
return false;
}
gfx::Rect rect;
if (!GetBoundingRectOfTargetElement(*it_xpath, parent_frame, &rect)) {
ADD_FAILURE() << "Failed to extract position of iframe with xpath `"
<< *it_xpath << "`!";
return false;
}
*offset += rect.OffsetFromOrigin();
frame = parent_frame;
}
return true;
}
bool TestRecipeReplayer::WaitForElementToBeReady(
const std::string& xpath,
const int visibility_enum_val,
content::RenderFrameHost* frame,
bool ignore_failure) {
std::vector<std::string> state_assertions;
state_assertions.push_back(base::StringPrintf(
"return automation_helper.isElementWithXpathReady(`%s`, %d);",
xpath.c_str(), visibility_enum_val));
return WaitForStateChange(
frame, state_assertions,
ignore_failure ? click_fallback_timeout : default_action_timeout,
ignore_failure);
}
bool TestRecipeReplayer::WaitForStateChange(
content::RenderFrameHost* frame,
const std::vector<std::string>& state_assertions,
const base::TimeDelta& timeout,
bool ignore_failure) {
base::TimeTicks start_time = base::TimeTicks::Now();
while (!AllAssertionsPassed(frame, state_assertions)) {
if (base::TimeTicks::Now() - start_time > timeout) {
if (!ignore_failure) {
ADD_FAILURE() << "State change hasn't completed within timeout.";
}
return false;
}
WaitTillPageIsIdle();
}
return true;
}
bool TestRecipeReplayer::AllAssertionsPassed(
const content::ToRenderFrameHost& frame,
const std::vector<std::string>& assertions) {
for (const std::string& assertion : assertions) {
if (!EvalJs(frame, base::StringPrintf("(function() {"
" try {"
" %s"
" } catch (ex) {}"
" return false;"
"})();",
assertion.c_str()))
.ExtractBool()) {
VLOG(1) << "'" << assertion << "' failed!";
return false;
}
}
return true;
}
bool TestRecipeReplayer::ExecuteJavaScriptOnElementByXpath(
const content::ToRenderFrameHost& frame,
const std::string& element_xpath,
const std::string& execute_function_body,
const base::TimeDelta& time_to_wait_for_element) {
std::string js(base::StringPrintf(
"try {"
" var element = automation_helper.getElementByXpath(`%s`);"
" (function(target) { %s })(element);"
"} catch(ex) {}",
element_xpath.c_str(), execute_function_body.c_str()));
return ExecJs(frame, js);
}
bool TestRecipeReplayer::GetElementProperty(
const content::ToRenderFrameHost& frame,
const std::string& element_xpath,
const std::string& get_property_function_body,
std::string* property) {
content::EvalJsResult result = content::EvalJs(
frame, base::StringPrintf(
"(function() {"
" var element = function() {"
" return automation_helper.getElementByXpath(`%s`);"
" }();"
" return function(target){%s}(element);})();",
element_xpath.c_str(), get_property_function_body.c_str()));
if (result.error.empty() && result.value.is_string()) {
*property = result.ExtractString();
return true;
}
*property = result.error;
return false;
}
bool TestRecipeReplayer::ExpectElementPropertyEqualsAnyOf(
const content::ToRenderFrameHost& frame,
const std::string& element_xpath,
const std::string& get_property_function_body,
const std::vector<std::string>& expected_values,
const std::string& validation_field,
IgnoreCase ignore_case) {
std::string actual_value;
if (!GetElementProperty(frame, element_xpath, get_property_function_body,
&actual_value)) {
ADD_FAILURE() << "Failed to extract element property! " << element_xpath
<< ", " << get_property_function_body;
return false;
}
auto is_expected = [ignore_case,
&actual_value](std::string_view expected_value) {
return ignore_case
? base::EqualsCaseInsensitiveASCII(expected_value, actual_value)
: expected_value == actual_value;
};
if (base::ranges::none_of(expected_values, is_expected)) {
std::string error_message = base::StrCat(
{"Field xpath: `", element_xpath, "` ", validation_field, ", ",
"Expected: '", base::JoinString(expected_values, " or "),
"', actual: '", actual_value, "'"});
VLOG(1) << error_message;
validation_failures_.push_back(testing::AssertionFailure()
<< error_message);
}
return true;
}
bool TestRecipeReplayer::ScrollElementIntoView(
const std::string& element_xpath,
content::RenderFrameHost* frame) {
const std::string scroll_target_js(base::StringPrintf(
"try {"
" const element = automation_helper.getElementByXpath(`%s`);"
" element.scrollIntoView({"
" block: 'center', inline: 'center'});"
" true;"
"} catch(ex) {"
" false;"
"}",
element_xpath.c_str()));
return EvalJs(frame, scroll_target_js).ExtractBool();
}
bool TestRecipeReplayer::PlaceFocusOnElement(
const std::string& element_xpath,
const std::vector<std::string> iframe_path,
content::RenderFrameHost* frame) {
if (!ScrollElementIntoView(element_xpath, frame))
return false;
const std::string focus_on_target_field_js(
base::StringPrintf("(function() {const element = "
"automation_helper.getElementByXpath(`%s`);"
" if (document.activeElement !== element) {"
" element.focus();"
" }"
" return document.activeElement === element;})();",
element_xpath.c_str()));
content::EvalJsResult result =
content::EvalJs(frame, focus_on_target_field_js);
if (result.error.empty() && result.value.is_bool() && result.ExtractBool()) {
return true;
} else {
VLOG(1) << "Failed to focus element through script:"
<< (result.error.empty()
? (result.value.is_bool() ? "Not a valid bool"
: "Returned false")
: result.error);
// Failing focusing on an element through script, use the less preferred
// method of left mouse clicking the element.
gfx::Rect rect;
if (!GetBoundingRectOfTargetElement(element_xpath, iframe_path, frame,
&rect))
return false;
return SimulateLeftMouseClickAt(rect.CenterPoint(), frame);
}
}
bool TestRecipeReplayer::GetBoundingRectOfTargetElement(
const std::string& target_element_xpath,
content::RenderFrameHost* frame,
gfx::Rect* output_rect) {
const std::string get_element_bounding_rect_js(base::StringPrintf(
"(function() {"
" try {"
" const element = automation_helper.getElementByXpath(`%s`);"
" const rect = element.getBoundingClientRect();"
" return Math.round(rect.left) + ',' + "
" Math.round(rect.top) + ',' + "
" Math.round(rect.width) + ',' + "
" Math.round(rect.height);"
" } catch(ex) {}"
" return '';"
"})();",
target_element_xpath.c_str()));
std::string rect_str =
content::EvalJs(frame, get_element_bounding_rect_js).ExtractString();
if (rect_str.empty()) {
ADD_FAILURE() << "Failed to extract target element's bounding rect!";
return false;
}
// Parse the bounding rect string to extract the element coordinates.
std::vector<std::string_view> rect_components = base::SplitStringPiece(
rect_str, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (rect_components.size() != 4) {
ADD_FAILURE() << "Wrong number of components in `" << rect_str << "`!";
return false;
}
int x = 0;
if (!base::StringToInt(rect_components[0], &x)) {
ADD_FAILURE() << "Failed to extract target element's x coordinate from "
<< "the string `" << rect_str[0] << "`!";
return false;
}
int y = 0;
if (!base::StringToInt(rect_components[1], &y)) {
ADD_FAILURE() << "Failed to extract target element's y coordinate from "
<< "the string `" << rect_str[1] << "`!";
return false;
}
int width = 0;
if (!base::StringToInt(rect_components[2], &width)) {
ADD_FAILURE() << "Failed to extract target element's width from "
<< "the string `" << rect_str[2] << "`!";
return false;
}
int height = 0;
if (!base::StringToInt(rect_components[3], &height)) {
ADD_FAILURE() << "Failed to extract target element's height from "
<< "the string `" << rect_str[3] << "`!";
return false;
}
output_rect->set_x(x);
output_rect->set_y(y);
output_rect->set_width(width);
output_rect->set_height(height);
return true;
}
bool TestRecipeReplayer::GetBoundingRectOfTargetElement(
const std::string& target_element_xpath,
const std::vector<std::string> iframe_path,
content::RenderFrameHost* frame,
gfx::Rect* output_rect) {
gfx::Vector2d offset;
if (!GetIFrameOffsetFromIFramePath(iframe_path, frame, &offset))
return false;
if (!GetBoundingRectOfTargetElement(target_element_xpath, frame, output_rect))
return false;
*output_rect += offset;
return true;
}
bool TestRecipeReplayer::SimulateLeftMouseClickAt(
const gfx::Point& point,
content::RenderFrameHost* render_frame_host) {
content::RenderWidgetHostView* view = render_frame_host->GetView();
if (!SimulateMouseHoverAt(render_frame_host, point))
return false;
blink::WebMouseEvent mouse_event(
blink::WebInputEvent::Type::kMouseDown,
blink::WebInputEvent::kNoModifiers,
blink::WebInputEvent::GetStaticTimeStampForTests());
mouse_event.button = blink::WebMouseEvent::Button::kLeft;
mouse_event.SetPositionInWidget(point.x(), point.y());
// Mac needs positionInScreen for events to plugins.
gfx::Rect offset =
content::WebContents::FromRenderFrameHost(render_frame_host)
->GetContainerBounds();
mouse_event.SetPositionInScreen(point.x() + offset.x(),
point.y() + offset.y());
mouse_event.click_count = 1;
content::RenderWidgetHost* widget = view->GetRenderWidgetHost();
widget->ForwardMouseEvent(mouse_event);
mouse_event.SetType(blink::WebInputEvent::Type::kMouseUp);
widget->ForwardMouseEvent(mouse_event);
return true;
}
bool TestRecipeReplayer::SimulateMouseHoverAt(
content::RenderFrameHost* render_frame_host,
const gfx::Point& point) {
gfx::Rect offset =
content::WebContents::FromRenderFrameHost(render_frame_host)
->GetContainerBounds();
gfx::Point reset_mouse =
gfx::Point(offset.x() + point.x(), offset.y() + point.y());
if (!ui_test_utils::SendMouseMoveSync(reset_mouse)) {
ADD_FAILURE() << "Failed to position the mouse!";
return false;
}
return true;
}
void TestRecipeReplayer::SimulateKeyPressWrapper(
content::WebContents* web_contents,
ui::DomKey key) {
ui::KeyboardCode key_code = ui::NonPrintableDomKeyToKeyboardCode(key);
ui::DomCode code = ui::UsLayoutKeyboardCodeToDomCode(key_code);
SimulateKeyPress(web_contents, key, code, key_code, false, false, false,
false);
}
bool TestRecipeReplayer::HasChromeStoredCredential(
const base::Value::Dict& action,
bool* stored_cred) {
std::optional<std::string> origin =
FindPopulateString(action, "origin", "Origin");
std::optional<std::string> username =
FindPopulateString(action, "userName", "Username");
std::optional<std::string> password =
FindPopulateString(action, "password", "Password");
if (!origin || !username || !password)
return false;
*stored_cred = feature_action_executor()->HasChromeStoredCredential(
*origin, *username, *password);
return true;
}
bool TestRecipeReplayer::SetupSavedAutofillProfile(
base::Value::List saved_autofill_profile_container) {
for (auto& list_entry : saved_autofill_profile_container) {
if (!list_entry.is_dict()) {
ADD_FAILURE() << "Failed to extract an entry!";
return false;
}
const base::Value::Dict list_entry_dict = std::move(list_entry).TakeDict();
std::optional<std::string> type =
FindPopulateString(list_entry_dict, "type", "profile field type");
std::optional<std::string> value =
FindPopulateString(list_entry_dict, "value", "profile field value");
if (!type || !value)
return false;
if (!feature_action_executor()->AddAutofillProfileInfo(*type, *value)) {
return false;
}
}
// Skip this step if autofill profile is empty.
// Only Autofill Captured Sites test recipes will have non-empty autofill
// profiles. Recipes for other captured sites tests will have empty autofill
// profiles. This block prevents these other tests from failing because
// the test feature action executor does not know how to setup the autofill
// profile.
if (saved_autofill_profile_container.empty()) {
return true;
}
return feature_action_executor()->SetupAutofillProfile();
}
bool TestRecipeReplayer::SetupSavedPasswords(
base::Value::List saved_password_list_container) {
for (auto& entry : saved_password_list_container) {
if (!entry.is_dict()) {
ADD_FAILURE() << "Failed to extract a saved password!";
return false;
}
const base::Value::Dict entry_dict = std::move(entry.GetDict());
std::optional<std::string> origin =
FindPopulateString(entry_dict, "website", "Website");
std::optional<std::string> username =
FindPopulateString(entry_dict, "username", "Username");
std::optional<std::string> password =
FindPopulateString(entry_dict, "password", "Password");
if (!origin || !username || !password)
return false;
if (!feature_action_executor()->AddCredential(*origin, *username,
*password)) {
return false;
}
}
return true;
}
// TestRecipeReplayChromeFeatureActionExecutor --------------------------------
TestRecipeReplayChromeFeatureActionExecutor::
TestRecipeReplayChromeFeatureActionExecutor() {}
TestRecipeReplayChromeFeatureActionExecutor::
~TestRecipeReplayChromeFeatureActionExecutor() {}
bool TestRecipeReplayChromeFeatureActionExecutor::AutofillForm(
const std::string& focus_element_css_selector,
const std::vector<std::string>& iframe_path,
const int attempts,
content::RenderFrameHost* frame,
std::optional<autofill::FieldType> triggered_field_type) {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor::AutofillForm "
"is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::AddAutofillProfileInfo(
const std::string& field_type,
const std::string& field_value) {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
"::AddAutofillProfileInfo is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::SetupAutofillProfile() {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
"::SetupAutofillProfile is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::AddCredential(
const std::string& origin,
const std::string& username,
const std::string& password) {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor::AddCredential"
" is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::SavePassword() {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor::SavePassword"
" is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::UpdatePassword() {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
"::UpdatePassword is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::WaitForSaveFallback() {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
"::WaitForSaveFallback is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::
IsChromeShowingPasswordGenerationPrompt() {
ADD_FAILURE()
<< "TestRecipeReplayChromeFeatureActionExecutor"
"::IsChromeShowingPasswordGenerationPrompt is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::
HasChromeShownSavePasswordPrompt() {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
"::HasChromeShownSavePasswordPrompt is not implemented!";
return false;
}
bool TestRecipeReplayChromeFeatureActionExecutor::HasChromeStoredCredential(
const std::string& origin,
const std::string& username,
const std::string& password) {
ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
"::HasChromeStoredCredential is not implemented!";
return false;
}
} // namespace captured_sites_test_utils