blob: 73f3f7d7dbfb9c9f82b6e82387d469c25e856497 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_string_value_serializer.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/strcat.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/permissions/permission_request_manager.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/app_modal/javascript_app_modal_dialog.h"
#include "components/app_modal/native_app_modal_dialog.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/navigation_handle.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 "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 maximum amount of time to wait for Chrome to finish autofilling a form.
const base::TimeDelta kAutofillActionWaitForVisualUpdateTimeout =
base::TimeDelta::FromSeconds(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=";
} // 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.
base::Value root_node;
{
JSONReader::ValueWithError value_with_error =
JSONReader().ReadAndReturnValueWithError(
json_text, JSONParserOptions::JSON_PARSE_RFC);
if (value_with_error.error_code !=
JSONReader::JsonParseError::JSON_NO_ERROR) {
LOG(WARNING) << "Could not load test config from json file: "
<< "`testcases.json` because: "
<< value_with_error.error_message;
return sites;
}
if (!value_with_error.value) {
LOG(WARNING) << "JSON Reader could not any object from `testcases.json`";
return sites;
}
root_node = std::move(value_with_error.value.value());
}
base::Value* list_node = root_node.FindListKey("tests");
if (!list_node) {
LOG(WARNING) << "No tests found in `testcases.json` config";
return sites;
}
bool also_run_disabled = testing::FLAGS_gtest_also_run_disabled_tests == 1;
for (auto& item : list_node->GetList()) {
const base::DictionaryValue* dict;
if (!item.GetAsDictionary(&dict))
continue;
CapturedSiteParams param;
param.site_name = *(dict->FindStringKey("site_name"));
if (dict->HasKey("scenario_dir"))
param.scenario_dir = *(dict->FindStringKey("scenario_dir"));
param.is_disabled = dict->FindBoolKey("disabled").value_or(false);
if (param.is_disabled && !also_run_disabled)
continue;
const std::string* expectation_string = dict->FindStringKey("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;
if (!param.scenario_dir.empty())
file_name = file_name.AppendASCII(param.scenario_dir);
file_name = file_name.AppendASCII(param.site_name);
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;
sites.push_back(param);
}
return sites;
}
std::string FilePathToUTF8(const base::FilePath::StringType& str) {
#if defined(OS_WIN)
return base::WideToUTF8(str);
#else
return str;
#endif
}
// 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 = FrameMatchingPredicate(
web_contents(), base::BindRepeating(&content::FrameMatchesName, name));
if (frame) {
return frame;
} else {
query_type_ = NAME;
frame_name_ = name;
base::ThreadTaskRunnerHandle::Get()->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 = FrameMatchingPredicate(
web_contents(), base::BindRepeating(&FrameHasOrigin, origin));
if (frame) {
return frame;
} else {
query_type_ = ORIGIN;
origin_ = origin;
base::ThreadTaskRunnerHandle::Get()->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 = FrameMatchingPredicate(
web_contents(), base::BindRepeating(&content::FrameHasSourceUrl, url));
if (frame) {
return frame;
} else {
query_type_ = URL;
url_ = url;
base::ThreadTaskRunnerHandle::Get()->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().GetOrigin() == 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.GetOrigin() == 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.GetOrigin() == origin.GetOrigin());
}
// TestRecipeReplayer ---------------------------------------------------------
TestRecipeReplayer::TestRecipeReplayer(
Browser* browser,
TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor)
: browser_(browser), feature_action_executor_(feature_action_executor) {}
TestRecipeReplayer::~TestRecipeReplayer() {}
bool TestRecipeReplayer::ReplayTest(const base::FilePath capture_file_path,
const base::FilePath recipe_file_path) {
if (!StartWebPageReplayServer(capture_file_path))
return false;
return ReplayRecordedActions(recipe_file_path);
}
const std::vector<testing::AssertionResult>
TestRecipeReplayer::GetValidationFailures() const {
return validation_failures_;
}
// static
void TestRecipeReplayer::SetUpCommandLine(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,"
// Uncomment to use the live autofill prediction server.
// "EXCLUDE clients1.google.com,"
"EXCLUDE localhost",
kHostHttpPort, kHostHttpsPort));
command_line->AppendSwitchASCII(
network::switches::kIgnoreCertificateErrorsSPKIList,
kWebPageReplayCertSPKI);
command_line->AppendSwitch(switches::kStartMaximized);
}
void TestRecipeReplayer::Setup() {
CleanupSiteData();
// Bypass permission dialogs.
PermissionRequestManager::FromWebContents(GetWebContents())
->set_auto_response_for_test(PermissionRequestManager::ACCEPT_ALL);
}
void TestRecipeReplayer::Cleanup() {
// 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(StopWebPageReplayServer())
<< "Cannot stop the local Web Page Replay server.";
}
TestRecipeReplayChromeFeatureActionExecutor*
TestRecipeReplayer::feature_action_executor() {
return feature_action_executor_;
}
Browser* TestRecipeReplayer::browser() {
return browser_;
}
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::ThreadTaskRunnerHandle::Get()->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::ThreadTaskRunnerHandle::Get()->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::ThreadTaskRunnerHandle::Get()->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.
ui_test_utils::NavigateToURL(browser_, GURL(url::kAboutBlankURL));
content::BrowsingDataRemover* remover =
content::BrowserContext::GetBrowsingDataRemover(browser_->profile());
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::StartWebPageReplayServer(
const base::FilePath& capture_file_path) {
std::vector<std::string> args;
base::FilePath src_dir;
if (!base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir)) {
ADD_FAILURE() << "Failed to extract the Chromium source directory!";
return false;
}
args.push_back(base::StringPrintf("--http_port=%d", kHostHttpPort));
args.push_back(base::StringPrintf("--https_port=%d", kHostHttpsPort));
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, this might make sense to comment out.
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("replay", args, &web_page_replay_server_))
return false;
// Sleep 5 seconds to wait for the web page replay server to start.
// TODO(crbug.com/847910): 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::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, wpr_launch_waiter.QuitClosure(),
base::TimeDelta::FromSeconds(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 TestRecipeReplayer::StopWebPageReplayServer() {
if (web_page_replay_server_.IsValid()) {
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 TestRecipeReplayer::RunWebPageReplayCmdAndWaitForExit(
const std::string& cmd,
const std::vector<std::string>& args,
const base::TimeDelta& timeout) {
base::Process process;
int exit_code;
if (RunWebPageReplayCmd(cmd, args, &process) && process.IsValid() &&
process.WaitForExitWithTimeout(timeout, &exit_code) && exit_code == 0) {
return true;
}
ADD_FAILURE() << "Failed to run WPR command: '" << cmd << "'!";
return false;
}
bool TestRecipeReplayer::RunWebPageReplayCmd(
const std::string& cmd,
const std::vector<std::string>& args,
base::Process* process) {
// 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_SOURCE_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 defined(OS_WIN)
base::FilePath wpr_executable_binary =
base::FilePath(FILE_PATH_LITERAL("win"))
.AppendASCII("AMD64")
.AppendASCII("wpr.exe");
#elif defined(OS_MACOSX)
base::FilePath wpr_executable_binary =
base::FilePath(FILE_PATH_LITERAL("mac"))
.AppendASCII("x86_64")
.AppendASCII("wpr");
#elif defined(OS_POSIX)
base::FilePath wpr_executable_binary =
base::FilePath(FILE_PATH_LITERAL("linux"))
.AppendASCII("x86_64")
.AppendASCII("wpr");
#else
#error Plaform is not supported.
#endif
base::CommandLine full_command(
web_page_replay_binary_dir.Append(wpr_executable_binary));
full_command.AppendArg(cmd);
// 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 offical
// WPR releases. The custom files are made to work on iOS.
base::FilePath src_dir;
if (!base::PathService::Get(base::DIR_SOURCE_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",
FilePathToUTF8(
web_page_replay_support_file_dir.AppendASCII("wpr_cert.pem").value())
.c_str()));
full_command.AppendArg(base::StringPrintf(
"--https_key_file=%s",
FilePathToUTF8(
web_page_replay_support_file_dir.AppendASCII("wpr_key.pem").value())
.c_str()));
for (const auto arg : args)
full_command.AppendArg(arg);
LOG(INFO) << full_command.GetArgumentsString();
*process = base::LaunchProcess(full_command, options);
return true;
}
const std::string* FindPopulateString(
const base::DictionaryValue& container,
const std::string key_name,
const std::string key_descriptor) {
const std::string* string_value = container.FindStringKey(key_name);
if (!string_value) {
ADD_FAILURE() << "Failed to extract '" << key_descriptor
<< "' from container!";
return nullptr;
}
return string_value;
}
bool TestRecipeReplayer::ReplayRecordedActions(
const base::FilePath& recipe_file_path) {
// Read the text of the recipe file.
base::ThreadRestrictions::SetIOAllowed(true);
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::unique_ptr<base::DictionaryValue> recipe = base::DictionaryValue::From(
base::JSONReader().ReadToValueDeprecated(json_text));
if (!recipe) {
ADD_FAILURE() << "Failed to deserialize json text!";
return false;
}
if (!InitializeBrowserToExecuteRecipe(recipe))
return false;
// Iterate through and execute each action in the recipe.
base::Value* action_list_container = recipe->FindListKey("actions");
if (!action_list_container) {
ADD_FAILURE() << "Failed to extract action list from the recipe!";
return false;
}
for (auto& item : action_list_container->GetList()) {
base::DictionaryValue* action;
if (!item.GetAsDictionary(&action)) {
ADD_FAILURE()
<< "Failed to extract an individual action from the recipe!";
return false;
}
const std::string* type =
FindPopulateString(*action, "type", "action type");
if (!type)
return false;
if (base::CompareCaseInsensitiveASCII(*type, "autofill") == 0) {
if (!ExecuteAutofillAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "click") == 0) {
if (!ExecuteClickAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "closeTab") == 0) {
if (!ExecuteCloseTabAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "coolOff") == 0) {
if (!ExecuteCoolOffAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "executeScript") == 0) {
if (!ExecuteRunCommandAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "hover") == 0) {
if (!ExecuteHoverAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "loadPage") == 0) {
if (!ExecuteForceLoadPage(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "pressEnter") == 0) {
if (!ExecutePressEnterAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "pressEscape") == 0) {
if (!ExecutePressEscapeAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "pressSpace") == 0) {
if (!ExecutePressSpaceAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "savePassword") == 0) {
if (!ExecuteSavePasswordAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "select") == 0) {
if (!ExecuteSelectDropdownAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "type") == 0) {
if (!ExecuteTypeAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "typePassword") == 0) {
if (!ExecuteTypePasswordAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "updatePassword") ==
0) {
if (!ExecuteUpdatePasswordAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "validateField") == 0) {
if (!ExecuteValidateFieldValueAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(
*type, "validateNoSavePasswordPrompt") == 0) {
if (!ExecuteValidateNoSavePasswordPromptAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(
*type, "validatePasswordSaveFallback") == 0) {
if (!ExecuteValidateSaveFallbackAction(*action))
return false;
} else if (base::CompareCaseInsensitiveASCII(*type, "waitFor") == 0) {
if (!ExecuteWaitForStateAction(*action))
return false;
} else {
ADD_FAILURE() << "Unrecognized action type: " << *type;
}
} // end foreach action
// Dismiss the beforeUnloadDialog if the last page of the test has a
// beforeUnload function.
if (recipe->FindKey("dismissBeforeUnload")) {
NavigateAwayAndDismissBeforeUnloadDialog();
}
return true;
}
// Functions for deserializing and executing actions from the test recipe
// JSON object.
bool TestRecipeReplayer::InitializeBrowserToExecuteRecipe(
const std::unique_ptr<base::DictionaryValue>& recipe) {
// Setup any saved address and credit card at the start of the test.
const base::Value* autofill_profile_container =
recipe->FindKey("autofillProfile");
if (autofill_profile_container &&
!SetupSavedAutofillProfile(*autofill_profile_container))
return false;
// Setup any saved passwords at the start of the test.
const base::Value* saved_password_container =
recipe->FindKey("passwordManagerProfiles");
if (saved_password_container &&
!SetupSavedPasswords(*saved_password_container))
return false;
// Extract the starting URL from the test recipe.
const std::string* starting_url = recipe->FindStringKey("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::ExecuteScript(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(
const base::DictionaryValue& 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;
}
if (!feature_action_executor()->AutofillForm(
xpath, frame_path, kAutofillActionNumRetries, frame))
return false;
WaitTillPageIsIdle(kAutofillActionWaitForVisualUpdateTimeout);
return true;
}
bool TestRecipeReplayer::ExecuteClickAction(
const base::DictionaryValue& 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::ExecuteCloseTabAction(
const base::DictionaryValue& action) {
VLOG(1) << "Closing Active Tab";
browser_->tab_strip_model()->CloseSelectedTabs();
return true;
}
bool TestRecipeReplayer::ExecuteCoolOffAction(
const base::DictionaryValue& action) {
base::RunLoop heart_beat;
base::TimeDelta cool_off_time = cool_off_action_timeout;
const base::Value* pause_time_container = action.FindKey("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::TimeDelta::FromSeconds(seconds);
}
base::ThreadTaskRunnerHandle::Get()->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(
const base::DictionaryValue& 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(
const base::DictionaryValue& action) {
bool shouldForce = action.FindBoolKey("force").value_or(false);
if (!shouldForce)
return true;
const std::string* url = FindPopulateString(action, "url", "Force Load URL");
if (!url)
return false;
VLOG(1) << "Making explicit URL redirect to '" << *url << "'";
ui_test_utils::NavigateToURL(browser_, GURL(*url));
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecutePressEnterAction(
const base::DictionaryValue& 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(
const base::DictionaryValue& action) {
VLOG(1) << "Pressing 'Esc' in the current frame";
SimulateKeyPressWrapper(GetWebContents(), ui::DomKey::ESCAPE);
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecutePressSpaceAction(
const base::DictionaryValue& 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(
const base::DictionaryValue& action) {
// Extract the list of JavaScript commands into a vector.
std::vector<std::string> commands;
const base::Value* list_container = action.FindListKey("commands");
if (!list_container) {
ADD_FAILURE() << "Failed to extract commands list from action";
return false;
}
for (const auto& command : list_container->GetList()) {
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::ExecuteScript(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(
const base::DictionaryValue& 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(
const base::DictionaryValue& action) {
base::Optional<int> index = action.FindIntKey("index");
if (!index) {
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.value() << "' from `" << xpath << "`.";
if (!ExecuteJavaScriptOnElementByXpath(
frame, xpath,
base::StringPrintf(
"automation_helper"
" .selectOptionFromDropDownElementByIndex(target, %d);",
index.value()))) {
ADD_FAILURE() << "Failed to select drop down option with JavaScript!";
return false;
}
WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteTypeAction(
const base::DictionaryValue& action) {
const 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(
const base::DictionaryValue& action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, true))
return false;
const 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(
const base::DictionaryValue& 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(
const base::DictionaryValue& action) {
std::string xpath;
content::RenderFrameHost* frame;
if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, false, true))
return false;
const base::Value* autofill_prediction_container =
action.FindKey("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 title;
if (GetElementProperty(frame, xpath, "return target.getAttribute('title');",
&title)) {
VLOG(1) << title;
} else {
ADD_FAILURE()
<< "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 << "'";
ExpectElementPropertyEquals(
frame, xpath,
"return target.getAttribute('autofill-prediction');",
expected_autofill_prediction_type, "autofill type mismatch", true);
}
const std::string* expected_value = FindPopulateString(action,
"expectedValue", "validation expected value");
if (!expected_value)
return false;
VLOG(1) << "Checking the field `" << xpath << "`.";
ExpectElementPropertyEquals(frame, xpath, "return target.value;",
*expected_value, "text value mismatch");
return true;
}
bool TestRecipeReplayer::ExecuteValidateNoSavePasswordPromptAction(
const base::DictionaryValue& action) {
VLOG(1) << "Verify that the page hasn't shown a save password prompt.";
EXPECT_FALSE(feature_action_executor()->HasChromeShownSavePasswordPrompt());
return true;
}
bool TestRecipeReplayer::ExecuteValidateSaveFallbackAction(
const base::DictionaryValue& 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(
const base::DictionaryValue& action) {
// Extract the list of JavaScript assertions into a vector.
std::vector<std::string> state_assertions;
const base::Value* list_container = action.FindListKey("assertions");
if (!list_container) {
ADD_FAILURE() << "Failed to extract wait assertions list from action";
return false;
}
for (const base::Value& assertion : list_container->GetList()) {
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::DictionaryValue& action,
std::string* xpath) {
xpath->clear();
const std::string* xpath_text =
FindPopulateString(action, "selector", "xpath selector");
if (!xpath_text)
return false;
*xpath = *xpath_text;
return true;
}
bool TestRecipeReplayer::GetTargetHTMLElementVisibilityEnumFromAction(
const base::DictionaryValue& action,
int* visibility_enum_val) {
const base::Value* visibility_container = action.FindKey("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::DictionaryValue& action,
content::RenderFrameHost** frame) {
const base::Value* iframe_container = action.FindKey("context");
if (!iframe_container) {
ADD_FAILURE() << "Failed to extract the iframe context from action!";
return false;
}
const base::DictionaryValue* iframe;
if (!iframe_container->GetAsDictionary(&iframe)) {
ADD_FAILURE() << "Failed to extract the iframe context object!";
return false;
}
base::Optional<bool> is_iframe_container = iframe->FindBoolKey("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()->GetMainFrame();
return true;
}
const base::Value* frame_name_container =
iframe->FindPath({"browserTest", "name"});
const base::Value* frame_origin_container =
iframe->FindPath({"browserTest", "origin"});
const base::Value* frame_url_container =
iframe->FindPath({"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::DictionaryValue& action,
std::string* xpath,
content::RenderFrameHost** frame,
bool set_focus,
bool relaxed_visibility) {
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))
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::DictionaryValue& action,
std::vector<std::string>* iframe_path) {
*iframe_path = std::vector<std::string>();
const base::Value* iframe_container = action.FindKey("context");
if (!iframe_container) {
ADD_FAILURE() << "Failed to extract the iframe context from action!";
return false;
}
const base::DictionaryValue* iframe;
if (!iframe_container->GetAsDictionary(&iframe)) {
ADD_FAILURE() << "Failed to extract the iframe context object!";
return false;
}
const base::Value* iframe_path_container = iframe->FindKey("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;
}
base::span<const base::Value> iframe_xpath_list =
iframe_path_container->GetList();
for (const auto& xpath : iframe_xpath_list) {
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) {
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, default_action_timeout);
}
bool TestRecipeReplayer::WaitForStateChange(
content::RenderFrameHost* frame,
const std::vector<std::string>& state_assertions,
const base::TimeDelta& timeout) {
base::TimeTicks start_time = base::TimeTicks::Now();
while (!AllAssertionsPassed(frame, state_assertions)) {
if (base::TimeTicks::Now() - start_time > timeout) {
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) {
bool assertion_passed = false;
EXPECT_TRUE(ExecuteScriptAndExtractBool(
frame,
base::StringPrintf("window.domAutomationController.send("
" (function() {"
" try {"
" %s"
" } catch (ex) {}"
" return false;"
" })());",
assertion.c_str()),
&assertion_passed));
if (!assertion_passed) {
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 ExecuteScript(frame, js);
}
bool TestRecipeReplayer::GetElementProperty(
const content::ToRenderFrameHost& frame,
const std::string& element_xpath,
const std::string& get_property_function_body,
std::string* property) {
return ExecuteScriptAndExtractString(
frame,
base::StringPrintf(
"window.domAutomationController.send("
" (function() {"
" try {"
" var element = function() {"
" return automation_helper.getElementByXpath(`%s`);"
" }();"
" return function(target){%s}(element);"
" } catch (ex) {}"
" return 'Exception encountered';"
" })());",
element_xpath.c_str(), get_property_function_body.c_str()),
property);
}
bool TestRecipeReplayer::ExpectElementPropertyEquals(
const content::ToRenderFrameHost& frame,
const std::string& element_xpath,
const std::string& get_property_function_body,
const std::string& expected_value,
const std::string& validation_field,
bool ignore_case) {
std::string value;
if (!GetElementProperty(frame, element_xpath, get_property_function_body,
&value)) {
ADD_FAILURE() << "Failed to extract element property! " << element_xpath
<< ", " << get_property_function_body;
return false;
}
if ((ignore_case &&
!base::EqualsCaseInsensitiveASCII(expected_value, value)) ||
(!ignore_case && expected_value != value)) {
std::string error_message = base::StrCat(
{"Field xpath: `", element_xpath, "` ", validation_field, ", ",
"Expected: '" , expected_value, "', 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'});"
" window.domAutomationController.send(true);"
"} catch(ex) {"
" window.domAutomationController.send(false);"
"}",
element_xpath.c_str()));
bool succeeded = false;
if (!ExecuteScriptAndExtractBool(frame, scroll_target_js, &succeeded)) {
ADD_FAILURE() << "Failed to scroll the element into view with JavaScript!";
return false;
}
return true;
}
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(
"try {"
" function onFocusHandler(event) {"
" event.target.removeEventListener(event.type, arguments.callee);"
" window.domAutomationController.send(true);"
" }"
" const element = automation_helper.getElementByXpath(`%s`);"
" if (document.activeElement === element) {"
" window.domAutomationController.send(true);"
" } else {"
" element.addEventListener('focus', onFocusHandler);"
" element.focus();"
" }"
" setTimeout(() => {"
" element.removeEventListener('focus', onFocusHandler);"
" window.domAutomationController.send(false);"
" }, 1000);"
"} catch(ex) {"
" window.domAutomationController.send(false);"
"}",
element_xpath.c_str()));
bool focused = false;
if (!ExecuteScriptAndExtractBool(frame, focus_on_target_field_js, &focused)) {
ADD_FAILURE() << "Failed to place focus on the element with JavaScript!";
return false;
}
if (focused) {
return true;
} else {
// 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) {
std::string rect_str;
const std::string get_element_bounding_rect_js(base::StringPrintf(
"window.domAutomationController.send("
" (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()));
if (!content::ExecuteScriptAndExtractString(
frame, get_element_bounding_rect_js, &rect_str)) {
ADD_FAILURE()
<< "Failed to run script to extract target element's bounding rect!";
return false;
}
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::istringstream rect_stream(rect_str);
std::string token;
if (!std::getline(rect_stream, token, ',')) {
ADD_FAILURE() << "Failed to extract target element's x coordinate from "
<< "the string `" << rect_str << "`!";
return false;
}
output_rect->set_x(std::stoi(token));
if (!std::getline(rect_stream, token, ',')) {
ADD_FAILURE() << "Failed to extract target element's y coordinate from "
<< "the string `" << rect_str << "`!";
return false;
}
output_rect->set_y(std::stoi(token));
if (!std::getline(rect_stream, token, ',')) {
ADD_FAILURE() << "Failed to extract target element's width from "
<< "the string `" << rect_str << "`!";
return false;
}
output_rect->set_width(std::stoi(token));
if (!std::getline(rect_stream, token, ',')) {
ADD_FAILURE() << "Failed to extract target element's height from "
<< "the string `" << rect_str << "`!";
return false;
}
output_rect->set_height(std::stoi(token));
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::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::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);
}
void TestRecipeReplayer::NavigateAwayAndDismissBeforeUnloadDialog() {
content::PrepContentsForBeforeUnloadTest(GetWebContents());
ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL(url::kAboutBlankURL), WindowOpenDisposition::CURRENT_TAB,
ui_test_utils::BROWSER_TEST_NONE);
app_modal::JavaScriptAppModalDialog* alert =
ui_test_utils::WaitForAppModalDialog();
alert->native_dialog()->AcceptAppModalDialog();
}
bool TestRecipeReplayer::HasChromeStoredCredential(
const base::DictionaryValue& action,
bool* stored_cred) {
const std::string* origin = FindPopulateString(action, "origin", "Origin");
const std::string* username = FindPopulateString(action,
"userName", "Username");
const 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(
const base::Value& saved_autofill_profile_container) {
if (!saved_autofill_profile_container.is_list()) {
ADD_FAILURE() << "Save Autofill Profile is not a list!";
return false;
}
base::span<const base::Value> profile_entries_list =
saved_autofill_profile_container.GetList();
for (auto it_entry = profile_entries_list.begin();
it_entry != profile_entries_list.end(); ++it_entry) {
const base::DictionaryValue* entry;
if (!it_entry->GetAsDictionary(&entry)) {
ADD_FAILURE() << "Failed to extract an entry!";
return false;
}
const std::string* type =
FindPopulateString(*entry, "type", "profile field type");
const std::string* value =
FindPopulateString(*entry, "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 (profile_entries_list.size() == 0) {
return true;
}
return feature_action_executor()->SetupAutofillProfile();
}
bool TestRecipeReplayer::SetupSavedPasswords(
const base::Value& saved_password_list_container) {
if (!saved_password_list_container.is_list()) {
ADD_FAILURE() << "Saved Password List is not a list!";
return false;
}
base::span<const base::Value> saved_password_list =
saved_password_list_container.GetList();
for (auto it_password = saved_password_list.begin();
it_password != saved_password_list.end(); ++it_password) {
const base::DictionaryValue* cred;
if (!it_password->GetAsDictionary(&cred)) {
ADD_FAILURE() << "Failed to extract a saved password!";
return false;
}
const std::string* origin =
FindPopulateString(*cred, "website", "Website");
const std::string* username =
FindPopulateString(*cred, "username", "Username");
const std::string* password =
FindPopulateString(*cred, "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) {
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::
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