blob: 47b2dfa9fcc955c888c000d79f1618bd098a3a05 [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 "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/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/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"
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;
} // namespace
namespace captured_sites_test_utils {
constexpr base::TimeDelta PageActivityObserver::kPaintEventCheckInterval;
std::string FilePathToUTF8(const base::FilePath::StringType& str) {
#if defined(OS_WIN)
return base::WideToUTF8(str);
#else
return str;
#endif
}
// PageActivityObserver -------------------------------------------------------
PageActivityObserver::PageActivityObserver(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
PageActivityObserver::PageActivityObserver(content::RenderFrameHost* frame)
: content::WebContentsObserver(
content::WebContents::FromRenderFrameHost(frame)) {}
void PageActivityObserver::WaitTillPageIsIdle(
base::TimeDelta continuous_paint_timeout) {
base::TimeTicks finished_load_time = base::TimeTicks::Now();
bool page_is_loading = false;
do {
paint_occurred_during_last_loop_ = false;
base::RunLoop heart_beat;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, heart_beat.QuitClosure(), kPaintEventCheckInterval);
heart_beat.Run();
page_is_loading =
web_contents()->IsWaitingForResponse() || web_contents()->IsLoading();
if (page_is_loading) {
finished_load_time = base::TimeTicks::Now();
} 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 making Chrome paint at
// regular intervals. Exit.
break;
}
} while (page_is_loading || paint_occurred_during_last_loop_);
}
bool PageActivityObserver::WaitForVisualUpdate(base::TimeDelta timeout) {
base::TimeTicks start_time = base::TimeTicks::Now();
while (!paint_occurred_during_last_loop_) {
base::RunLoop heart_beat;
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, heart_beat.QuitClosure(), kPaintEventCheckInterval);
heart_beat.Run();
if ((base::TimeTicks::Now() - start_time) > timeout) {
return false;
}
}
return true;
}
void PageActivityObserver::DidCommitAndDrawCompositorFrame() {
paint_occurred_during_last_loop_ = true;
}
// 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);
}
// 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));
}
void TestRecipeReplayer::Setup() {
EXPECT_TRUE(InstallWebPageReplayServerRootCert())
<< "Cannot install the root certificate "
<< "for the local web page replay server.";
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.";
EXPECT_TRUE(RemoveWebPageReplayServerRootCert())
<< "Cannot remove the root certificate "
<< "for 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::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(base::StringPrintf(
"--inject_scripts=%s,%s",
FilePathToUTF8(
src_dir.AppendASCII("third_party/catapult/web_page_replay_go")
.AppendASCII("deterministic.js")
.value())
.c_str(),
FilePathToUTF8(
src_dir
.AppendASCII("chrome/test/data/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 20 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(20));
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::InstallWebPageReplayServerRootCert() {
return RunWebPageReplayCmdAndWaitForExit("installroot",
std::vector<std::string>());
}
bool TestRecipeReplayer::RemoveWebPageReplayServerRootCert() {
return RunWebPageReplayCmdAndWaitForExit("removeroot",
std::vector<std::string>());
}
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) {
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/catapult/telemetry/telemetry/internal/bin");
options.current_directory = web_page_replay_binary_dir;
#if defined(OS_WIN)
std::string wpr_executable_binary = "win/x86_64/wpr";
#elif defined(OS_MACOSX)
std::string wpr_executable_binary = "mac/x86_64/wpr";
#elif defined(OS_POSIX)
std::string wpr_executable_binary = "linux/x86_64/wpr";
#else
#error Plaform is not supported.
#endif
base::CommandLine full_command(
web_page_replay_binary_dir.AppendASCII(wpr_executable_binary));
full_command.AppendArg(cmd);
// Ask web page replay to use the custom certifcate 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/test/data/autofill/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);
*process = base::LaunchProcess(full_command, options);
return true;
}
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().ReadToValue(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->FindKey("actions");
if (!action_list_container) {
ADD_FAILURE() << "Failed to extract action list from the recipe!";
return false;
}
if (base::Value::Type::LIST != action_list_container->type()) {
ADD_FAILURE() << "The recipe's actions object is not a list!";
return false;
}
base::Value::ListStorage& action_list = action_list_container->GetList();
for (auto it_action = action_list.begin(); it_action != action_list.end();
++it_action) {
base::DictionaryValue* action;
if (!it_action->GetAsDictionary(&action)) {
ADD_FAILURE()
<< "Failed to extract an individual action from the recipe!";
return false;
}
base::Value* type_container = action->FindKey("type");
if (!type_container) {
ADD_FAILURE() << "Failed to extract action type from the recipe!";
return false;
}
if (base::Value::Type::STRING != type_container->type()) {
ADD_FAILURE() << "Action type is not a string!";
return false;
}
std::string type = type_container->GetString();
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, "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) {
// Load page is an no-op action.
} else if (base::CompareCaseInsensitiveASCII(type, "pressEnter") == 0) {
if (!ExecutePressEnterAction(*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(
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.
base::Value* starting_url_container = recipe->FindKey("startingURL");
if (!starting_url_container) {
ADD_FAILURE() << "Failed to extract the starting url from the recipe!";
return false;
}
if (base::Value::Type::STRING != starting_url_container->type()) {
ADD_FAILURE() << "Starting url is not a string!";
return false;
}
std::string starting_url = starting_url_container->GetString();
// Navigate to the starting URL, wait for the page to complete loading.
PageActivityObserver page_activity_observer(GetWebContents());
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;
}
page_activity_observer.WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteAutofillAction(
const base::DictionaryValue& action) {
std::string xpath;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
std::vector<std::string> frame_path;
if (!GetIFramePathFromAction(action, &frame_path))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
VLOG(1) << "Invoking Chrome Autofill on `" << xpath << "`.";
PageActivityObserver page_activity_observer(frame);
// 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(frame, xpath, frame_path,
kAutofillActionNumRetries))
return false;
page_activity_observer.WaitTillPageIsIdle(
kAutofillActionWaitForVisualUpdateTimeout);
return true;
}
bool TestRecipeReplayer::ExecuteClickAction(
const base::DictionaryValue& action) {
std::string xpath;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
VLOG(1) << "Left mouse clicking `" << xpath << "`.";
PageActivityObserver page_activity_observer(frame);
if (!ExecuteJavaScriptOnElementByXpath(frame, xpath, "target.click();")) {
ADD_FAILURE() << "Failed to left click element with JavaScript!";
return false;
}
page_activity_observer.WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteHoverAction(
const base::DictionaryValue& action) {
std::string xpath;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
VLOG(1) << "Hovering over `" << xpath << "`.";
PageActivityObserver page_activity_observer(frame);
gfx::Rect rect;
if (!GetBoundingRectOfTargetElement(frame, xpath, &rect))
return false;
if (!SimulateMouseHoverAt(frame, rect.CenterPoint()))
return false;
if (!page_activity_observer.WaitForVisualUpdate()) {
ADD_FAILURE() << "The page did not respond to a mouse hover action!";
return false;
}
return true;
}
bool TestRecipeReplayer::ExecutePressEnterAction(
const base::DictionaryValue& action) {
std::string xpath;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
std::vector<std::string> frame_path;
if (!GetIFramePathFromAction(action, &frame_path))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
VLOG(1) << "Press 'Enter' on `" << xpath << "`.";
PageActivityObserver page_activity_observer(frame);
if (!PlaceFocusOnElement(frame, xpath, frame_path))
return false;
ui::DomKey key = ui::DomKey::ENTER;
ui::KeyboardCode key_code = ui::NonPrintableDomKeyToKeyboardCode(key);
ui::DomCode code = ui::UsLayoutKeyboardCodeToDomCode(key_code);
SimulateKeyPress(content::WebContents::FromRenderFrameHost(frame), key, code,
key_code, false, false, false, false);
page_activity_observer.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* commands_list_container = action.FindKey("commands");
if (!commands_list_container) {
ADD_FAILURE()
<< "Failed to extract the list of commands from the run command "
<< "action!";
return false;
}
if (base::Value::Type::LIST != commands_list_container->type()) {
ADD_FAILURE() << "commands is not an array!";
return false;
}
const base::Value::ListStorage& commands_list =
commands_list_container->GetList();
for (auto it_command = commands_list.begin();
it_command != commands_list.end(); ++it_command) {
if (base::Value::Type::STRING != it_command->type()) {
ADD_FAILURE() << "command is not a string!";
return false;
}
commands.push_back(it_command->GetString());
}
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame)) {
return false;
}
VLOG(1) << "Running JavaScript commands on the page.";
// Execute the commands.
PageActivityObserver page_activity_observer(frame);
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.
page_activity_observer.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) {
const base::Value* index_container = action.FindKey("index");
if (!index_container) {
ADD_FAILURE()
<< "Failed to extract selection index from the select action!";
return false;
}
if (base::Value::Type::INTEGER != index_container->type()) {
ADD_FAILURE() << "Selection index is not an integer!";
return false;
}
int index = index_container->GetInt();
std::string xpath;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
VLOG(1) << "Select option '" << index << "' from `" << xpath << "`.";
PageActivityObserver page_activity_observer(frame);
if (!ExecuteJavaScriptOnElementByXpath(
frame, xpath,
base::StringPrintf(
"automation_helper"
" .selectOptionFromDropDownElementByIndex(target, %d);",
index_container->GetInt()))) {
ADD_FAILURE() << "Failed to select drop down option with JavaScript!";
return false;
}
page_activity_observer.WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteTypeAction(
const base::DictionaryValue& action) {
const base::Value* value_container = action.FindKey("value");
if (!value_container) {
ADD_FAILURE() << "Failed to extract value from the type action!";
return false;
}
if (base::Value::Type::STRING != value_container->type()) {
ADD_FAILURE() << "Value is not a string!";
return false;
}
std::string value = value_container->GetString();
std::string xpath;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
VLOG(1) << "Typing '" << value << "' inside `" << xpath << "`.";
PageActivityObserver page_activity_observer(frame);
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;
}
page_activity_observer.WaitTillPageIsIdle();
return true;
}
bool TestRecipeReplayer::ExecuteTypePasswordAction(
const base::DictionaryValue& action) {
std::string xpath;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
std::vector<std::string> frame_path;
if (!GetIFramePathFromAction(action, &frame_path))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
const base::Value* value_container = action.FindKey("value");
if (!value_container) {
ADD_FAILURE() << "Failed to extract the value from the type password"
<< " action!";
return false;
}
if (base::Value::Type::STRING != value_container->type()) {
ADD_FAILURE() << "Value is not a string!";
return false;
}
std::string value = value_container->GetString();
// 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;
}
if (!PlaceFocusOnElement(frame, xpath, frame_path))
return false;
VLOG(1) << "Typing '" << value << "' inside `" << xpath << "`.";
const char* c_array = value.c_str();
for (size_t index = 0; index < value.size(); index++) {
ui::DomKey key = ui::DomKey::FromCharacter(c_array[index]);
ui::KeyboardCode key_code = ui::NonPrintableDomKeyToKeyboardCode(key);
ui::DomCode code = ui::UsLayoutKeyboardCodeToDomCode(key_code);
SimulateKeyPress(content::WebContents::FromRenderFrameHost(frame), key,
code, key_code, false, false, false, false);
}
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;
if (!GetTargetHTMLElementXpathFromAction(action, &xpath))
return false;
int visibility_enum_val;
if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
&visibility_enum_val))
return false;
content::RenderFrameHost* frame;
if (!GetTargetFrameFromAction(action, &frame))
return false;
if (!WaitForElementToBeReady(frame, xpath, visibility_enum_val))
return false;
const base::Value* autofill_prediction_container =
action.FindKey("expectedAutofillType");
if (autofill_prediction_container) {
if (base::Value::Type::STRING != autofill_prediction_container->type()) {
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.c_str(),
"return target.getAttribute('autofill-prediction');",
expected_autofill_prediction_type, true);
}
const base::Value* expected_value_container = action.FindKey("expectedValue");
if (!expected_value_container) {
ADD_FAILURE() << "Failed to extract the expected value field from the "
"validate field value action!";
return false;
}
if (base::Value::Type::STRING != expected_value_container->type()) {
ADD_FAILURE() << "Expected value is not a string!";
return false;
}
std::string expected_value = expected_value_container->GetString();
VLOG(1) << "Checking the field `" << xpath << "`.";
ExpectElementPropertyEquals(frame, xpath.c_str(), "return target.value;",
expected_value);
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* assertions_list_container = action.FindKey("assertions");
if (!assertions_list_container) {
ADD_FAILURE()
<< "Failed to extract assertions from the wait for state action!";
return false;
}
if (base::Value::Type::LIST != assertions_list_container->type()) {
ADD_FAILURE() << "Assertions is not a list!";
return false;
}
const base::Value::ListStorage& assertions_list =
assertions_list_container->GetList();
for (const base::Value& assertion : assertions_list) {
if (base::Value::Type::STRING != assertion.type()) {
ADD_FAILURE() << "Assertion is not a string!";
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 base::Value* xpath_container = action.FindKey("selector");
if (!xpath_container) {
ADD_FAILURE() << "Failed to extract the xpath selector from action!";
return false;
}
if (base::Value::Type::STRING != xpath_container->type()) {
ADD_FAILURE() << "Xpath selector is not a string!";
return false;
}
*xpath = xpath_container->GetString();
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 (base::Value::Type::INTEGER != visibility_container->type()) {
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;
}
const base::Value* is_iframe_container = iframe->FindKey("isIframe");
if (!is_iframe_container) {
ADD_FAILURE()
<< "Failed to extract the isIframe field from the iframe context!";
return false;
}
if (base::Value::Type::BOOLEAN != is_iframe_container->type()) {
ADD_FAILURE() << "isIframe is not a boolean value!";
return false;
}
if (!is_iframe_container->GetBool()) {
*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 &&
base::Value::Type::STRING != frame_name_container->type()) {
ADD_FAILURE() << "Iframe name is not a string!";
return false;
}
if (frame_origin_container != nullptr &&
base::Value::Type::STRING != frame_origin_container->type()) {
ADD_FAILURE() << "Iframe origin is not a string!";
return false;
}
if (frame_url_container != nullptr &&
base::Value::Type::STRING != frame_url_container->type()) {
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::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 (base::Value::Type::LIST != iframe_path_container->type()) {
ADD_FAILURE() << "The action's iframe path is not a list!";
return false;
}
const base::Value::ListStorage& iframe_xpath_list =
iframe_path_container->GetList();
for (auto it_xpath = iframe_xpath_list.begin();
it_xpath != iframe_xpath_list.end(); ++it_xpath) {
std::string xpath;
if (!it_xpath->GetAsString(&xpath)) {
ADD_FAILURE() << "Failed to extract the iframe xpath from action!";
return false;
}
iframe_path->push_back(xpath);
}
return true;
}
bool TestRecipeReplayer::GetIFrameOffsetFromIFramePath(
content::RenderFrameHost* frame,
const std::vector<std::string>& iframe_path,
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(parent_frame, *it_xpath, &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(
content::RenderFrameHost* frame,
const std::string& xpath,
const int visibility_enum_val) {
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) {
const base::TimeTicks start_time = base::TimeTicks::Now();
PageActivityObserver page_activity_observer(
content::WebContents::FromRenderFrameHost(frame));
while (!AllAssertionsPassed(frame, state_assertions)) {
if (base::TimeTicks::Now() - start_time > timeout) {
ADD_FAILURE() << "State change hasn't completed within timeout.";
return false;
}
page_activity_observer.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,
bool ignoreCase) {
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 (ignoreCase) {
EXPECT_TRUE(base::EqualsCaseInsensitiveASCII(expected_value, value))
<< "Field xpath: `" << element_xpath << "`, "
<< "Expected: " << expected_value << ", actual: " << value;
} else {
EXPECT_EQ(expected_value, value)
<< "Field xpath: `" << element_xpath << "`, ";
}
return true;
}
bool TestRecipeReplayer::PlaceFocusOnElement(
content::RenderFrameHost* frame,
const std::string& element_xpath,
const std::vector<std::string> iframe_path) {
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`);"
" element.scrollIntoView({"
" block: 'center', inline: 'center'});"
" 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(frame, element_xpath, iframe_path,
&rect))
return false;
return SimulateLeftMouseClickAt(frame, rect.CenterPoint());
}
}
bool TestRecipeReplayer::GetBoundingRectOfTargetElement(
content::RenderFrameHost* frame,
const std::string& target_element_xpath,
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(
content::RenderFrameHost* frame,
const std::string& target_element_xpath,
const std::vector<std::string> iframe_path,
gfx::Rect* output_rect) {
gfx::Vector2d offset;
if (!GetIFrameOffsetFromIFramePath(frame, iframe_path, &offset))
return false;
if (!GetBoundingRectOfTargetElement(frame, target_element_xpath, output_rect))
return false;
*output_rect += offset;
return true;
}
bool TestRecipeReplayer::SimulateLeftMouseClickAt(
content::RenderFrameHost* render_frame_host,
const gfx::Point& point) {
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::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 base::Value* orgin_container = action.FindKey("origin");
if (!orgin_container) {
ADD_FAILURE() << "Failed to extract the origin from the action!";
return false;
}
if (base::Value::Type::STRING != orgin_container->type()) {
ADD_FAILURE() << "Origin is not a string!";
return false;
}
const base::Value* user_name_container = action.FindKey("userName");
if (!user_name_container) {
ADD_FAILURE() << "Failed to extract the user name from the action!";
return false;
}
if (base::Value::Type::STRING != user_name_container->type()) {
ADD_FAILURE() << "User name is not a string!";
return false;
}
const base::Value* password_container = action.FindKey("password");
if (!password_container) {
ADD_FAILURE() << "Failed to extract the password from the action!";
return false;
}
if (base::Value::Type::STRING != password_container->type()) {
ADD_FAILURE() << "Password is not a string!";
return false;
}
*stored_cred = feature_action_executor()->HasChromeStoredCredential(
orgin_container->GetString(), user_name_container->GetString(),
password_container->GetString());
return true;
}
bool TestRecipeReplayer::SetupSavedAutofillProfile(
const base::Value& saved_autofill_profile_container) {
if (base::Value::Type::LIST != saved_autofill_profile_container.type()) {
ADD_FAILURE() << "Save Autofill Profile is not a list!";
return false;
}
const base::Value::ListStorage& 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 base::Value* type_container = entry->FindKey("type");
if (base::Value::Type::STRING != type_container->type()) {
ADD_FAILURE() << "Type is not a string!";
return false;
}
const std::string type = type_container->GetString();
const base::Value* value_container = entry->FindKey("value");
if (base::Value::Type::STRING != value_container->type()) {
ADD_FAILURE() << "Value is not a string!";
return false;
}
const std::string value = value_container->GetString();
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 (base::Value::Type::LIST != saved_password_list_container.type()) {
ADD_FAILURE() << "Saved Password List is not a list!";
return false;
}
const base::Value::ListStorage& 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 base::Value* origin_container = cred->FindKey("website");
if (base::Value::Type::STRING != origin_container->type()) {
ADD_FAILURE() << "Website is not a string!";
return false;
}
const std::string origin = origin_container->GetString();
const base::Value* username_container = cred->FindKey("username");
if (base::Value::Type::STRING != username_container->type()) {
ADD_FAILURE() << "User name is not a string!";
return false;
}
const std::string username = username_container->GetString();
const base::Value* password_container = cred->FindKey("password");
if (base::Value::Type::STRING != password_container->type()) {
ADD_FAILURE() << "User name is not a string!";
return false;
}
const std::string password = password_container->GetString();
if (!feature_action_executor()->AddCredential(origin, username, password)) {
return false;
}
}
return true;
}
// TestRecipeReplayChromeFeatureActionExecutor --------------------------------
TestRecipeReplayChromeFeatureActionExecutor::
TestRecipeReplayChromeFeatureActionExecutor() {}
TestRecipeReplayChromeFeatureActionExecutor::
~TestRecipeReplayChromeFeatureActionExecutor() {}
bool TestRecipeReplayChromeFeatureActionExecutor::AutofillForm(
content::RenderFrameHost* frame,
const std::string& focus_element_css_selector,
const std::vector<std::string> iframe_path,
const int attempts) {
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