Commit 831f175d authored by Yiming Zhou's avatar Yiming Zhou Committed by Commit Bot

Refactor captures sites automation framework out of autofill tests.

This change refactors the captured sites automation framework code out
of autofill_captured_sites_interactive_uitest. By completing this
refactor, other Chrome teams can leverage the captures sites
automation framework to test their team's features on real-world sites.

Also included in this change are a few minor fixes:
1. Fixed the captured sites autofill test to work with EventWaiters.
2. Modified a few test recipe files to address test breaks caused by
removing the client name parameter from autofill server queries.

Bug: 866152
Change-Id: Iee47cee8bd038ce0920caf6d63440c90c94b6285
Reviewed-on: https://chromium-review.googlesource.com/1147512
Commit-Queue: Yiming Zhou <uwyiming@google.com>
Reviewed-by: default avatarSebastien Seguin-Gagnon <sebsg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#580278}
parent 90b244eb
...@@ -7,10 +7,7 @@ ...@@ -7,10 +7,7 @@
#include "base/command_line.h" #include "base/command_line.h"
#include "base/files/file_enumerator.h" #include "base/files/file_enumerator.h"
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/guid.h" #include "base/guid.h"
#include "base/json/json_reader.h"
#include "base/json/json_string_value_serializer.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/path_service.h" #include "base/path_service.h"
#include "base/strings/string16.h" #include "base/strings/string16.h"
...@@ -23,17 +20,17 @@ ...@@ -23,17 +20,17 @@
#include "build/build_config.h" #include "build/build_config.h"
#include "chrome/browser/autofill/autofill_uitest.h" #include "chrome/browser/autofill/autofill_uitest.h"
#include "chrome/browser/autofill/autofill_uitest_util.h" #include "chrome/browser/autofill/autofill_uitest_util.h"
#include "chrome/browser/browser_process.h" #include "chrome/browser/autofill/captured_sites_test_utils.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/autofill/chrome_autofill_client.h" #include "chrome/browser/ui/autofill/chrome_autofill_client.h"
#include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_features.h" #include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h" #include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h" #include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h" #include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/test_switches.h" #include "chrome/test/base/test_switches.h"
#include "chrome/test/base/ui_test_utils.h" #include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
#include "components/autofill/content/browser/content_autofill_driver_factory.h" #include "components/autofill/content/browser/content_autofill_driver_factory.h"
#include "components/autofill/core/browser/autofill_manager.h" #include "components/autofill/core/browser/autofill_manager.h"
#include "components/autofill/core/browser/autofill_manager_test_delegate.h" #include "components/autofill/core/browser/autofill_manager_test_delegate.h"
...@@ -44,10 +41,7 @@ ...@@ -44,10 +41,7 @@
#include "components/autofill/core/browser/state_names.h" #include "components/autofill/core/browser/state_names.h"
#include "components/autofill/core/common/autofill_features.h" #include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/autofill_switches.h" #include "components/autofill/core/common/autofill_switches.h"
#include "content/public/browser/browsing_data_remover.h" #include "components/autofill/core/common/autofill_util.h"
#include "content/public/browser/web_contents.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_renderer_host.h"
#include "content/public/test/test_utils.h" #include "content/public/test/test_utils.h"
#include "services/network/public/cpp/network_switches.h" #include "services/network/public/cpp/network_switches.h"
...@@ -55,72 +49,18 @@ ...@@ -55,72 +49,18 @@
namespace { namespace {
const base::TimeDelta default_action_timeout = base::TimeDelta::FromSeconds(30); const base::TimeDelta autofill_wait_for_action_interval =
const base::TimeDelta paint_event_check_interval = base::TimeDelta::FromSeconds(5);
base::TimeDelta::FromMilliseconds(500);
const int autofill_action_num_retries = 5;
// PageActivityObserver waits until Chrome finishes loading a page and stops
// making visual updates to the page.
class PageActivityObserver : public content::WebContentsObserver {
public:
explicit PageActivityObserver(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
~PageActivityObserver() override = default;
// Wait until Chrome finishes loading a page and updating the page's visuals.
// If Chrome finishes loading a page but continues to paint every half
// second, exit after |continuous_paint_timeout| expires since Chrome
// finished loading the page.
void WaitTillPageIsIdle(
base::TimeDelta continuous_paint_timeout = default_action_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(), paint_event_check_interval);
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_);
}
private:
void DidCommitAndDrawCompositorFrame() override {
paint_occurred_during_last_loop_ = true;
}
bool paint_occurred_during_last_loop_ = false;
DISALLOW_COPY_AND_ASSIGN(PageActivityObserver);
};
std::string FilePathToUTF8(const base::FilePath::StringType& str) {
#if defined(OS_WIN)
return base::WideToUTF8(str);
#else
return str;
#endif
}
base::FilePath GetReplayFilesDirectory() { base::FilePath GetReplayFilesDirectory() {
base::FilePath src_dir; base::FilePath src_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir)); if (base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir)) {
return src_dir.Append( return src_dir.Append(
FILE_PATH_LITERAL("chrome/test/data/autofill/captured_sites")); FILE_PATH_LITERAL("chrome/test/data/autofill/captured_sites"));
} else {
src_dir.clear();
return src_dir;
}
} }
// Iterate through Autofill's Web Page Replay capture file directory to look // Iterate through Autofill's Web Page Replay capture file directory to look
...@@ -138,7 +78,8 @@ std::vector<std::string> GetCapturedSites() { ...@@ -138,7 +78,8 @@ std::vector<std::string> GetCapturedSites() {
// has the '.test' extension. // has the '.test' extension.
if (file.Extension().empty() && if (file.Extension().empty() &&
base::PathExists(file.AddExtension(FILE_PATH_LITERAL(".test")))) { base::PathExists(file.AddExtension(FILE_PATH_LITERAL(".test")))) {
sites.push_back(FilePathToUTF8(file.BaseName().value())); sites.push_back(
captured_sites_test_utils::FilePathToUTF8(file.BaseName().value()));
} }
} }
std::sort(sites.begin(), sites.end()); std::sort(sites.begin(), sites.end());
...@@ -151,33 +92,78 @@ namespace autofill { ...@@ -151,33 +92,78 @@ namespace autofill {
class AutofillCapturedSitesInteractiveTest class AutofillCapturedSitesInteractiveTest
: public AutofillUiTest, : public AutofillUiTest,
public captured_sites_test_utils::
TestRecipeReplayChromeFeatureActionExecutor,
public ::testing::WithParamInterface<std::string> { public ::testing::WithParamInterface<std::string> {
public:
// TestRecipeReplayChromeFeatureActionExecutor
bool AutofillForm(content::WebContents* web_contents,
const std::string& focus_element_css_selector,
int attempts = 1) override {
AutofillManager* autofill_manager =
ContentAutofillDriverFactory::FromWebContents(web_contents)
->DriverForFrame(web_contents->GetMainFrame())
->autofill_manager();
autofill_manager->SetTestDelegate(test_delegate());
int tries = 0;
while (tries < attempts) {
tries++;
autofill_manager->client()->HideAutofillPopup();
if (!ShowAutofillSuggestion(focus_element_css_selector)) {
LOG(WARNING) << "Failed to bring up the autofill suggestion drop down.";
continue;
}
if (!ShouldAutoselectFirstSuggestionOnArrowDown()) {
// Press the down key to highlight the first choice in the autofill
// suggestion drop down.
test_delegate()->Reset();
SendKeyToPopup(ui::DomKey::ARROW_DOWN);
if (!test_delegate()->Wait({ObservedUiEvents::kPreviewFormData},
autofill_wait_for_action_interval)) {
LOG(WARNING) << "Failed to select an option from the "
<< "autofill suggestion drop down.";
continue;
}
}
// Press the enter key to invoke autofill using the first suggestion.
test_delegate()->Reset();
SendKeyToPopup(ui::DomKey::ENTER);
if (!test_delegate()->Wait({ObservedUiEvents::kFormDataFilled},
autofill_wait_for_action_interval)) {
LOG(WARNING) << "Failed to fill the form.";
continue;
}
return true;
}
autofill_manager->client()->HideAutofillPopup();
return false;
}
protected: protected:
AutofillCapturedSitesInteractiveTest() AutofillCapturedSitesInteractiveTest()
: profile_(test::GetFullProfile()), : profile_(test::GetFullProfile()),
card_(CreditCard(base::GenerateGUID(), "http://www.example.com")) {} card_(CreditCard(base::GenerateGUID(), "http://www.example.com")) {}
~AutofillCapturedSitesInteractiveTest() override {} ~AutofillCapturedSitesInteractiveTest() override {}
// InProcessBrowserTest: // InProcessBrowserTest:
void SetUpOnMainThread() override { void SetUpOnMainThread() override {
AutofillUiTest::SetUpOnMainThread(); AutofillUiTest::SetUpOnMainThread();
SetupTestProfile(); SetupTestProfile();
EXPECT_TRUE(InstallWebPageReplayServerRootCert()) recipe_replayer_ =
<< "Cannot install the root certificate " std::make_unique<captured_sites_test_utils::TestRecipeReplayer>(
<< "for the local web page replay server."; browser(), this);
CleanupSiteData(); recipe_replayer()->Setup();
} }
void TearDownOnMainThread() override { void TearDownOnMainThread() override {
// If there are still cookies at the time the browser test shuts down, recipe_replayer()->Cleanup();
// 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.";
AutofillUiTest::TearDownOnMainThread(); AutofillUiTest::TearDownOnMainThread();
} }
...@@ -188,120 +174,12 @@ class AutofillCapturedSitesInteractiveTest ...@@ -188,120 +174,12 @@ class AutofillCapturedSitesInteractiveTest
// elements in a form to determine if the form is ready for interaction. // elements in a form to determine if the form is ready for interaction.
feature_list_.InitAndEnableFeature(features::kAutofillShowTypePredictions); feature_list_.InitAndEnableFeature(features::kAutofillShowTypePredictions);
command_line->AppendSwitch(switches::kShowAutofillTypePredictions); command_line->AppendSwitch(switches::kShowAutofillTypePredictions);
captured_sites_test_utils::TestRecipeReplayer::SetUpCommandLine(
// Direct traffic to the Web Page Replay server. command_line);
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",
host_http_port_, host_https_port_));
} }
bool StartWebPageReplayServer(const std::string& replay_file) { captured_sites_test_utils::TestRecipeReplayer* recipe_replayer() {
std::vector<std::string> args; return recipe_replayer_.get();
base::FilePath src_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
args.push_back(base::StringPrintf("--http_port=%d", host_http_port_));
args.push_back(base::StringPrintf("--https_port=%d", host_https_port_));
args.push_back(base::StringPrintf(
"--inject_scripts=%s,%s",
FilePathToUTF8(src_dir
.Append(FILE_PATH_LITERAL(
"third_party/catapult/web_page_replay_go"))
.Append(FILE_PATH_LITERAL("deterministic.js"))
.value())
.c_str(),
FilePathToUTF8(
src_dir
.Append(FILE_PATH_LITERAL(
"chrome/test/data/web_page_replay_go_helper_scripts"))
.Append(FILE_PATH_LITERAL("automation_helper.js"))
.value())
.c_str()));
// Specify the replay file.
args.push_back(base::StringPrintf(
"%s", FilePathToUTF8(
GetReplayFilesDirectory().AppendASCII(replay_file).value())
.c_str()));
web_page_replay_server_ = RunWebPageReplayCmd("replay", args);
// Sleep 20 seconds to wait for the web page replay server to start.
// TODO(bug 847910): create a process std stream reader class to use the
// process output to determine when the server is ready.
base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(20));
return web_page_replay_server_.IsValid();
}
bool StopWebPageReplayServer() {
if (web_page_replay_server_.IsValid())
return web_page_replay_server_.Terminate(0, true);
// The test server hasn't started, no op.
return true;
}
bool ReplayTestRecipe(const char* recipe_file_name) {
// Read the text of the recipe file.
base::FilePath src_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
base::FilePath recipe_file_path = GetReplayFilesDirectory().AppendASCII(
base::StringPrintf("%s.test", recipe_file_name));
base::ThreadRestrictions::SetIOAllowed(true);
std::string json_text;
CHECK(base::ReadFileToString(recipe_file_path, &json_text));
// 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;
}
InitializeBrowserToExecuteRecipe(recipe);
// Iterate through and execute each action in the recipe.
base::Value* action_list_container = recipe->FindKey("actions");
CHECK(action_list_container);
CHECK_EQ(base::Value::Type::LIST, action_list_container->type());
base::Value::ListStorage& action_list = action_list_container->GetList();
for (base::ListValue::iterator it_action = action_list.begin();
it_action != action_list.end(); ++it_action) {
base::DictionaryValue* action;
CHECK(it_action->GetAsDictionary(&action));
base::Value* type_container = action->FindKey("type");
CHECK(type_container);
CHECK_EQ(base::Value::Type::STRING, type_container->type());
std::string type = type_container->GetString();
if (base::CompareCaseInsensitiveASCII(type, "waitFor") == 0) {
ExecuteWaitForStateAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "execute") == 0) {
ExecuteRunCommandAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "click") == 0) {
ExecuteClickAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "type") == 0) {
ExecuteTypeAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "select") == 0) {
ExecuteSelectDropdownAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "autofill") == 0) {
ExecuteAutofillAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "validateField") ==
0) {
ExecuteValidateFieldValueAction(action);
} else {
ADD_FAILURE() << "Unrecognized action type: " << type;
}
} // end foreach action
return true;
} }
const CreditCard credit_card() { return card_; } const CreditCard credit_card() { return card_; }
...@@ -319,369 +197,56 @@ class AutofillCapturedSitesInteractiveTest ...@@ -319,369 +197,56 @@ class AutofillCapturedSitesInteractiveTest
AddTestAutofillData(browser(), profile_, card_); AddTestAutofillData(browser(), profile_, card_);
} }
bool InstallWebPageReplayServerRootCert() { bool ShowAutofillSuggestion(const std::string& focus_element_xpath) {
return RunWebPageReplayCmdAndWaitForExit("installroot", const std::string js(base::StringPrintf(
std::vector<std::string>());
}
bool RemoveWebPageReplayServerRootCert() {
return RunWebPageReplayCmdAndWaitForExit("removeroot",
std::vector<std::string>());
}
bool RunWebPageReplayCmdAndWaitForExit(
const std::string& cmd,
const std::vector<std::string>& args,
const base::TimeDelta& timeout = base::TimeDelta::FromSeconds(5)) {
base::Process process = RunWebPageReplayCmd(cmd, args);
if (process.IsValid()) {
int exit_code;
if (process.WaitForExitWithTimeout(timeout, &exit_code))
return (exit_code == 0);
}
return false;
}
base::Process RunWebPageReplayCmd(const std::string& cmd,
const std::vector<std::string>& args) {
base::LaunchOptions options = base::LaunchOptionsForTest();
base::FilePath exe_dir;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &exe_dir));
base::FilePath web_page_replay_binary_dir =
exe_dir.Append(FILE_PATH_LITERAL(
"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;
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
base::FilePath web_page_replay_support_file_dir =
src_dir.Append(FILE_PATH_LITERAL(
"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
.Append(FILE_PATH_LITERAL("wpr_cert.pem"))
.value())
.c_str()));
full_command.AppendArg(base::StringPrintf(
"--https_key_file=%s",
FilePathToUTF8(web_page_replay_support_file_dir
.Append(FILE_PATH_LITERAL("wpr_key.pem"))
.value())
.c_str()));
for (auto const& arg : args)
full_command.AppendArg(arg);
return base::LaunchProcess(full_command, options);
}
bool WaitForStateChange(
const std::vector<std::string>& state_assertions,
const base::TimeDelta& timeout = default_action_timeout) {
const base::TimeTicks start_time = base::TimeTicks::Now();
while (!AllAssertionsPassed(state_assertions)) {
if (base::TimeTicks::Now() - start_time > timeout) {
ADD_FAILURE() << "State change hasn't completed within timeout.";
return false;
}
base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
}
return true;
}
bool AllAssertionsPassed(const std::vector<std::string>& assertions)
WARN_UNUSED_RESULT {
for (std::string const& assertion : assertions) {
bool assertion_passed = false;
EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
GetWebContents(),
base::StringPrintf("window.domAutomationController.send("
" (function() {"
" try {"
" %s"
" } catch (ex) {}"
" return false;"
" })());",
assertion.c_str()),
&assertion_passed));
if (!assertion_passed) {
LOG(ERROR) << "'" << assertion << "' failed!";
return false;
}
}
return true;
}
void 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 ExecuteJavaScriptOnElementByXpath(
const std::string& element_xpath,
const std::string& execute_function_body,
const base::TimeDelta& time_to_wait_for_element =
default_action_timeout) {
std::string js(base::StringPrintf(
"try {" "try {"
" var element = automation_helper.getElementByXpath(`%s`);" " var element = automation_helper.getElementByXpath(`%s`);"
" (function(target) { %s })(element);" " while (document.activeElement !== element) {"
" element.focus();"
" }"
"} catch(ex) {}", "} catch(ex) {}",
element_xpath.c_str(), execute_function_body.c_str())); focus_element_xpath.c_str()));
return content::ExecuteScript(GetWebContents(), js); if (content::ExecuteScript(GetWebContents(), js)) {
} test_delegate()->Reset();
bool ExpectElementPropertyEquals( if (ShouldAutoselectFirstSuggestionOnArrowDown()) {
const std::string& element_xpath, SendKeyToPage(ui::DomKey::ARROW_DOWN);
const std::string& get_property_function_body, return test_delegate()->Wait({ObservedUiEvents::kSuggestionShown,
const std::string& expected_value, ObservedUiEvents::kPreviewFormData},
bool ignoreCase = false) { autofill_wait_for_action_interval);
std::string value;
if (content::ExecuteScriptAndExtractString(
GetWebContents(),
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()),
&value)) {
if (ignoreCase) {
EXPECT_TRUE(base::EqualsCaseInsensitiveASCII(expected_value, value))
<< "Field xpath: `" << element_xpath << "`, "
<< "Expected: " << expected_value << ", actual: " << value;
} else { } else {
EXPECT_EQ(expected_value, value) SendKeyToPage(ui::DomKey::ARROW_DOWN);
<< "Field xpath: `" << element_xpath << "`, "; return test_delegate()->Wait({ObservedUiEvents::kSuggestionShown},
autofill_wait_for_action_interval);
} }
return true;
} }
LOG(ERROR) << element_xpath << ", " << get_property_function_body;
return false; return false;
} }
// Functions for deserializing and executing actions from the test recipe
// JSON object.
void InitializeBrowserToExecuteRecipe(
std::unique_ptr<base::DictionaryValue>& recipe) {
// Extract the starting URL from the test recipe.
base::Value* starting_url_container = recipe->FindKey("startingURL");
CHECK(starting_url_container);
CHECK_EQ(base::Value::Type::STRING, starting_url_container->type());
LOG(INFO) << "Navigating to " << starting_url_container->GetString();
// Navigate to the starting URL, wait for the page to complete loading.
PageActivityObserver page_activity_observer(GetWebContents());
CHECK(content::ExecuteScript(
GetWebContents(),
base::StringPrintf("window.location.href = '%s';",
starting_url_container->GetString().c_str())));
page_activity_observer.WaitTillPageIsIdle();
}
void ExecuteWaitForStateAction(base::DictionaryValue* action) {
// Extract the list of JavaScript assertions into a vector.
std::vector<std::string> state_assertions;
base::Value* assertions_list_container = action->FindKey("assertions");
CHECK(assertions_list_container);
CHECK_EQ(base::Value::Type::LIST, assertions_list_container->type());
base::Value::ListStorage& assertions_list =
assertions_list_container->GetList();
for (base::ListValue::iterator it_assertion = assertions_list.begin();
it_assertion != assertions_list.end(); ++it_assertion) {
CHECK_EQ(base::Value::Type::STRING, it_assertion->type());
state_assertions.push_back(it_assertion->GetString());
}
LOG(INFO) << "Waiting for page to reach a state.";
// Wait for all of the assertions to become true on the current page.
CHECK(WaitForStateChange(state_assertions, default_action_timeout));
}
void ExecuteRunCommandAction(base::DictionaryValue* action) {
// Extract the list of JavaScript commands into a vector.
std::vector<std::string> commands;
base::Value* commands_list_container = action->FindKey("commands");
CHECK(commands_list_container);
CHECK_EQ(base::Value::Type::LIST, commands_list_container->type());
base::Value::ListStorage& commands_list =
commands_list_container->GetList();
for (base::ListValue::iterator it_command = commands_list.begin();
it_command != commands_list.end(); ++it_command) {
CHECK_EQ(base::Value::Type::STRING, it_command->type());
commands.push_back(it_command->GetString());
}
LOG(INFO) << "Running JavaScript commands on the page.";
// Execute the commands.
PageActivityObserver page_activity_observer(GetWebContents());
for (const std::string& command : commands) {
CHECK(content::ExecuteScript(GetWebContents(), command));
// Wait in case the JavaScript command triggers page load or layout
// changes.
page_activity_observer.WaitTillPageIsIdle();
}
}
void ExecuteClickAction(base::DictionaryValue* action) {
std::string xpath = GetTargetHTMLElementXpathFromAction(action);
WaitForElemementToBeReady(xpath);
LOG(INFO) << "Left mouse clicking `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
CHECK(ExecuteJavaScriptOnElementByXpath(xpath, "target.click();"));
page_activity_observer.WaitTillPageIsIdle();
}
void ExecuteTypeAction(base::DictionaryValue* action) {
base::Value* value_container = action->FindKey("value");
CHECK(value_container);
CHECK_EQ(base::Value::Type::STRING, value_container->type());
std::string value = value_container->GetString();
std::string xpath = GetTargetHTMLElementXpathFromAction(action);
WaitForElemementToBeReady(xpath);
LOG(INFO) << "Typing '" << value << "' inside `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
CHECK(ExecuteJavaScriptOnElementByXpath(
xpath, base::StringPrintf(
"automation_helper.setInputElementValue(target, `%s`);",
value.c_str())));
page_activity_observer.WaitTillPageIsIdle();
}
void ExecuteSelectDropdownAction(base::DictionaryValue* action) {
base::Value* index_container = action->FindKey("index");
CHECK(index_container);
CHECK_EQ(base::Value::Type::INTEGER, index_container->type());
int index = index_container->GetInt();
std::string xpath = GetTargetHTMLElementXpathFromAction(action);
WaitForElemementToBeReady(xpath);
LOG(INFO) << "Select option '" << index << "' from `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
CHECK(ExecuteJavaScriptOnElementByXpath(
xpath, base::StringPrintf(
"automation_helper"
" .selectOptionFromDropDownElementByIndex(target, %d);",
index_container->GetInt())));
page_activity_observer.WaitTillPageIsIdle();
}
void ExecuteAutofillAction(base::DictionaryValue* action) {
std::string xpath = GetTargetHTMLElementXpathFromAction(action);
WaitForElemementToBeReady(xpath);
LOG(INFO) << "Invoking Chrome Autofill on `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
// 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.
CHECK(ExecuteJavaScriptOnElementByXpath(
xpath, "automation_helper.setInputElementValue(target, ``);"));
CHECK(TryFillForm(xpath, autofill_action_num_retries));
page_activity_observer.WaitTillPageIsIdle();
}
void ExecuteValidateFieldValueAction(base::DictionaryValue* action) {
base::Value* autofill_prediction_container =
action->FindKey("expectedAutofillType");
CHECK(autofill_prediction_container);
CHECK_EQ(base::Value::Type::STRING, autofill_prediction_container->type());
std::string expected_autofill_prediction_type =
autofill_prediction_container->GetString();
base::Value* expected_value_container = action->FindKey("expectedValue");
CHECK(expected_value_container);
CHECK_EQ(base::Value::Type::STRING, expected_value_container->type());
std::string expected_value = expected_value_container->GetString();
std::string xpath = GetTargetHTMLElementXpathFromAction(action);
WaitForElemementToBeReady(xpath);
LOG(INFO) << "Checking the field `" << xpath << "`.";
ExpectElementPropertyEquals(
xpath.c_str(), "return target.getAttribute('autofill-prediction');",
expected_autofill_prediction_type, true);
ExpectElementPropertyEquals(xpath.c_str(), "return target.value;",
expected_value);
}
std::string GetTargetHTMLElementXpathFromAction(
base::DictionaryValue* action) {
base::Value* xpath_container = action->FindKey("selector");
CHECK(xpath_container);
CHECK_EQ(base::Value::Type::STRING, xpath_container->type());
return xpath_container->GetString();
}
void WaitForElemementToBeReady(std::string xpath) {
std::vector<std::string> state_assertions;
state_assertions.push_back(base::StringPrintf(
"return automation_helper.isElementWithXpathReady(`%s`);",
xpath.c_str()));
CHECK(WaitForStateChange(state_assertions, default_action_timeout));
}
// The Web Page Replay server that will be serving the captured sites
base::Process web_page_replay_server_;
const int host_http_port_ = 8080;
const int host_https_port_ = 8081;
AutofillProfile profile_; AutofillProfile profile_;
CreditCard card_; CreditCard card_;
std::unique_ptr<captured_sites_test_utils::TestRecipeReplayer>
recipe_replayer_;
base::test::ScopedFeatureList feature_list_; base::test::ScopedFeatureList feature_list_;
}; };
IN_PROC_BROWSER_TEST_P(AutofillCapturedSitesInteractiveTest, Recipe) { IN_PROC_BROWSER_TEST_P(AutofillCapturedSitesInteractiveTest, Recipe) {
// Prints the path of the test to be executed. // Prints the path of the test to be executed.
LOG(INFO) << GetParam(); VLOG(1) << GetParam();
ASSERT_TRUE(StartWebPageReplayServer(GetParam()));
ASSERT_TRUE(ReplayTestRecipe(GetParam().c_str())); // Craft the capture file path.
base::FilePath src_dir;
ASSERT_TRUE(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
base::FilePath capture_file_path =
GetReplayFilesDirectory().AppendASCII(GetParam().c_str());
// Craft the recipe file path.
base::FilePath recipe_file_path = GetReplayFilesDirectory().AppendASCII(
base::StringPrintf("%s.test", GetParam().c_str()));
ASSERT_TRUE(
recipe_replayer()->ReplayTest(capture_file_path, recipe_file_path));
} }
struct GetParamAsString { struct GetParamAsString {
......
...@@ -89,70 +89,6 @@ void AutofillUiTest::TearDownOnMainThread() { ...@@ -89,70 +89,6 @@ void AutofillUiTest::TearDownOnMainThread() {
test::ReenableSystemServices(); test::ReenableSystemServices();
} }
bool AutofillUiTest::TryFillForm(const std::string& focus_element_xpath,
const int attempts) {
content::WebContents* web_contents = GetWebContents();
AutofillManager* autofill_manager =
ContentAutofillDriverFactory::FromWebContents(web_contents)
->DriverForFrame(web_contents->GetMainFrame())
->autofill_manager();
int tries = 0;
while (tries < attempts) {
tries++;
autofill_manager->client()->HideAutofillPopup();
if (!ShowAutofillSuggestion(focus_element_xpath)) {
LOG(WARNING) << "Failed to bring up the autofill suggestion drop down.";
continue;
}
// Press the down key again to highlight the first choice in the autofill
// suggestion drop down.
test_delegate()->Reset();
SendKeyToPopup(ui::DomKey::ARROW_DOWN);
if (!test_delegate()->Wait({ObservedUiEvents::kPreviewFormData},
base::TimeDelta::FromSeconds(5))) {
LOG(WARNING) << "Failed to select an option from the"
<< " autofill suggestion drop down.";
continue;
}
// Press the enter key to invoke autofill using the first suggestion.
test_delegate()->Reset();
SendKeyToPopup(ui::DomKey::ENTER);
if (!test_delegate()->Wait({ObservedUiEvents::kFormDataFilled},
base::TimeDelta::FromSeconds(5))) {
LOG(WARNING) << "Failed to fill the form.";
continue;
}
return true;
}
autofill_manager->client()->HideAutofillPopup();
return false;
}
bool AutofillUiTest::ShowAutofillSuggestion(
const std::string& focus_element_xpath) {
const std::string js(base::StringPrintf(
"try {"
" var element = automation_helper.getElementByXpath(`%s`);"
" while (document.activeElement !== element) {"
" element.focus();"
" }"
"} catch(ex) {}",
focus_element_xpath.c_str()));
if (content::ExecuteScript(GetWebContents(), js)) {
test_delegate()->Reset();
SendKeyToPage(ui::DomKey::ARROW_DOWN);
return test_delegate()->Wait({ObservedUiEvents::kSuggestionShown},
base::TimeDelta::FromSeconds(5));
}
return false;
}
void AutofillUiTest::SendKeyToPage(ui::DomKey key) { void AutofillUiTest::SendKeyToPage(ui::DomKey key) {
ui::KeyboardCode key_code = ui::NonPrintableDomKeyToKeyboardCode(key); ui::KeyboardCode key_code = ui::NonPrintableDomKeyToKeyboardCode(key);
ui::DomCode code = ui::UsLayoutKeyboardCodeToDomCode(key_code); ui::DomCode code = ui::UsLayoutKeyboardCodeToDomCode(key_code);
......
...@@ -64,10 +64,6 @@ class AutofillUiTest : public InProcessBrowserTest { ...@@ -64,10 +64,6 @@ class AutofillUiTest : public InProcessBrowserTest {
void SetUpOnMainThread() override; void SetUpOnMainThread() override;
void TearDownOnMainThread() override; void TearDownOnMainThread() override;
bool TryFillForm(const std::string& focus_element_xpath,
const int attempts = 1);
bool ShowAutofillSuggestion(const std::string& focus_element_xpath);
void SendKeyToPage(ui::DomKey key); void SendKeyToPage(ui::DomKey key);
void SendKeyToPageAndWait(ui::DomKey key, void SendKeyToPageAndWait(ui::DomKey key,
std::list<ObservedUiEvents> expected_events); std::list<ObservedUiEvents> expected_events);
...@@ -75,6 +71,7 @@ class AutofillUiTest : public InProcessBrowserTest { ...@@ -75,6 +71,7 @@ class AutofillUiTest : public InProcessBrowserTest {
ui::DomCode code, ui::DomCode code,
ui::KeyboardCode key_code, ui::KeyboardCode key_code,
std::list<ObservedUiEvents> expected_events); std::list<ObservedUiEvents> expected_events);
void SendKeyToPopup(ui::DomKey key);
// Send key to the render host view's widget if |widget| is null. // Send key to the render host view's widget if |widget| is null.
void SendKeyToPopupAndWait(ui::DomKey key, void SendKeyToPopupAndWait(ui::DomKey key,
std::list<ObservedUiEvents> expected_events, std::list<ObservedUiEvents> expected_events,
...@@ -98,8 +95,6 @@ class AutofillUiTest : public InProcessBrowserTest { ...@@ -98,8 +95,6 @@ class AutofillUiTest : public InProcessBrowserTest {
content::RenderWidgetHost::KeyPressEventCallback key_press_event_sink(); content::RenderWidgetHost::KeyPressEventCallback key_press_event_sink();
private: private:
void SendKeyToPopup(ui::DomKey key);
AutofillManagerTestDelegateImpl test_delegate_; AutofillManagerTestDelegateImpl test_delegate_;
// KeyPressEventCallback that serves as a sink to ensure that every key press // KeyPressEventCallback that serves as a sink to ensure that every key press
......
// 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/profiles/profile.h"
#include "chrome/test/base/ui_test_utils.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"
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_);
}
void PageActivityObserver::DidCommitAndDrawCompositorFrame() {
paint_occurred_during_last_loop_ = true;
}
// 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 ReplayRecordedActions(recipe_file_path);
}
return false;
}
// 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();
}
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_;
}
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))
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();
return web_page_replay_server_.IsValid();
}
bool TestRecipeReplayer::StopWebPageReplayServer() {
if (web_page_replay_server_.IsValid())
return web_page_replay_server_.Terminate(0, true);
// 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;
if (!RunWebPageReplayCmd(cmd, args, &process))
return false;
if (process.IsValid()) {
int exit_code;
if (process.WaitForExitWithTimeout(timeout, &exit_code))
return (exit_code == 0);
}
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))
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))
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))
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;
}
InitializeBrowserToExecuteRecipe(recipe);
// Iterate through and execute each action in the recipe.
base::Value* action_list_container = recipe->FindKey("actions");
if (!action_list_container)
return false;
if (base::Value::Type::LIST != action_list_container->type())
return false;
base::Value::ListStorage& action_list = action_list_container->GetList();
for (base::ListValue::iterator it_action = action_list.begin();
it_action != action_list.end(); ++it_action) {
base::DictionaryValue* action;
if (!it_action->GetAsDictionary(&action))
return false;
base::Value* type_container = action->FindKey("type");
if (!type_container)
return false;
if (base::Value::Type::STRING != type_container->type())
return false;
std::string type = type_container->GetString();
if (base::CompareCaseInsensitiveASCII(type, "autofill") == 0) {
ExecuteAutofillAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "click") == 0) {
ExecuteClickAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "select") == 0) {
ExecuteSelectDropdownAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "type") == 0) {
ExecuteTypeAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "validateField") == 0) {
ExecuteValidateFieldValueAction(action);
} else if (base::CompareCaseInsensitiveASCII(type, "waitFor") == 0) {
ExecuteWaitForStateAction(action);
} else {
ADD_FAILURE() << "Unrecognized action type: " << type;
}
} // end foreach action
return true;
}
// Functions for deserializing and executing actions from the test recipe
// JSON object.
void TestRecipeReplayer::InitializeBrowserToExecuteRecipe(
std::unique_ptr<base::DictionaryValue>& recipe) {
// Extract the starting URL from the test recipe.
base::Value* starting_url_container = recipe->FindKey("startingURL");
ASSERT_TRUE(starting_url_container);
ASSERT_EQ(base::Value::Type::STRING, starting_url_container->type());
// Navigate to the starting URL, wait for the page to complete loading.
PageActivityObserver page_activity_observer(GetWebContents());
ASSERT_TRUE(content::ExecuteScript(
GetWebContents(),
base::StringPrintf("window.location.href = '%s';",
starting_url_container->GetString().c_str())));
page_activity_observer.WaitTillPageIsIdle();
}
void TestRecipeReplayer::ExecuteAutofillAction(base::DictionaryValue* action) {
std::string xpath;
ASSERT_TRUE(GetTargetHTMLElementXpathFromAction(action, &xpath));
WaitForElemementToBeReady(xpath);
VLOG(1) << "Invoking Chrome Autofill on `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
// 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.
ASSERT_TRUE(ExecuteJavaScriptOnElementByXpath(
xpath, "automation_helper.setInputElementValue(target, ``);"));
ASSERT_TRUE(feature_action_executor()->AutofillForm(
GetWebContents(), xpath, kAutofillActionNumRetries));
page_activity_observer.WaitTillPageIsIdle(
kAutofillActionWaitForVisualUpdateTimeout);
}
void TestRecipeReplayer::ExecuteClickAction(base::DictionaryValue* action) {
std::string xpath;
ASSERT_TRUE(GetTargetHTMLElementXpathFromAction(action, &xpath));
WaitForElemementToBeReady(xpath);
VLOG(1) << "Left mouse clicking `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
ASSERT_TRUE(ExecuteJavaScriptOnElementByXpath(xpath, "target.click();"));
page_activity_observer.WaitTillPageIsIdle();
}
void TestRecipeReplayer::ExecuteSelectDropdownAction(
base::DictionaryValue* action) {
base::Value* index_container = action->FindKey("index");
ASSERT_TRUE(index_container);
ASSERT_EQ(base::Value::Type::INTEGER, index_container->type());
int index = index_container->GetInt();
std::string xpath;
ASSERT_TRUE(GetTargetHTMLElementXpathFromAction(action, &xpath));
WaitForElemementToBeReady(xpath);
VLOG(1) << "Select option '" << index << "' from `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
ASSERT_TRUE(ExecuteJavaScriptOnElementByXpath(
xpath, base::StringPrintf(
"automation_helper"
" .selectOptionFromDropDownElementByIndex(target, %d);",
index_container->GetInt())));
page_activity_observer.WaitTillPageIsIdle();
}
void TestRecipeReplayer::ExecuteTypeAction(base::DictionaryValue* action) {
base::Value* value_container = action->FindKey("value");
ASSERT_TRUE(value_container);
ASSERT_EQ(base::Value::Type::STRING, value_container->type());
std::string value = value_container->GetString();
std::string xpath;
ASSERT_TRUE(GetTargetHTMLElementXpathFromAction(action, &xpath));
WaitForElemementToBeReady(xpath);
VLOG(1) << "Typing '" << value << "' inside `" << xpath << "`.";
PageActivityObserver page_activity_observer(GetWebContents());
ASSERT_TRUE(ExecuteJavaScriptOnElementByXpath(
xpath, base::StringPrintf(
"automation_helper.setInputElementValue(target, `%s`);",
value.c_str())));
page_activity_observer.WaitTillPageIsIdle();
}
void TestRecipeReplayer::ExecuteValidateFieldValueAction(
base::DictionaryValue* action) {
std::string xpath;
ASSERT_TRUE(GetTargetHTMLElementXpathFromAction(action, &xpath));
WaitForElemementToBeReady(xpath);
base::Value* autofill_prediction_container =
action->FindKey("expectedAutofillType");
if (autofill_prediction_container) {
ASSERT_EQ(base::Value::Type::STRING, autofill_prediction_container->type());
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(
xpath.c_str(), "return target.getAttribute('autofill-prediction');",
expected_autofill_prediction_type, true);
}
base::Value* expected_value_container = action->FindKey("expectedValue");
ASSERT_TRUE(expected_value_container);
ASSERT_EQ(base::Value::Type::STRING, expected_value_container->type());
std::string expected_value = expected_value_container->GetString();
VLOG(1) << "Checking the field `" << xpath << "`.";
ExpectElementPropertyEquals(xpath.c_str(), "return target.value;",
expected_value);
}
void TestRecipeReplayer::ExecuteWaitForStateAction(
base::DictionaryValue* action) {
// Extract the list of JavaScript assertions into a vector.
std::vector<std::string> state_assertions;
base::Value* assertions_list_container = action->FindKey("assertions");
ASSERT_TRUE(assertions_list_container);
ASSERT_EQ(base::Value::Type::LIST, assertions_list_container->type());
base::Value::ListStorage& assertions_list =
assertions_list_container->GetList();
for (base::ListValue::iterator it_assertion = assertions_list.begin();
it_assertion != assertions_list.end(); ++it_assertion) {
ASSERT_EQ(base::Value::Type::STRING, it_assertion->type());
state_assertions.push_back(it_assertion->GetString());
}
VLOG(1) << "Waiting for page to reach a state.";
// Wait for all of the assertions to become true on the current page.
ASSERT_TRUE(WaitForStateChange(state_assertions, default_action_timeout));
}
bool TestRecipeReplayer::GetTargetHTMLElementXpathFromAction(
base::DictionaryValue* action,
std::string* xpath) {
xpath->clear();
base::Value* xpath_container = action->FindKey("selector");
if (!xpath_container)
return false;
if (base::Value::Type::STRING != xpath_container->type())
return false;
*xpath = xpath_container->GetString();
return true;
}
void TestRecipeReplayer::WaitForElemementToBeReady(std::string xpath) {
std::vector<std::string> state_assertions;
state_assertions.push_back(base::StringPrintf(
"return automation_helper.isElementWithXpathReady(`%s`);",
xpath.c_str()));
ASSERT_TRUE(WaitForStateChange(state_assertions, default_action_timeout));
}
bool TestRecipeReplayer::WaitForStateChange(
const std::vector<std::string>& state_assertions,
const base::TimeDelta& timeout) {
const base::TimeTicks start_time = base::TimeTicks::Now();
PageActivityObserver page_activity_observer(GetWebContents());
while (!AllAssertionsPassed(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 std::vector<std::string>& assertions) {
for (const std::string& assertion : assertions) {
bool assertion_passed = false;
EXPECT_TRUE(ExecuteScriptAndExtractBool(
GetWebContents(),
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 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(GetWebContents(), js);
}
bool TestRecipeReplayer::ExpectElementPropertyEquals(
const std::string& element_xpath,
const std::string& get_property_function_body,
const std::string& expected_value,
bool ignoreCase) {
std::string value;
if (ExecuteScriptAndExtractString(
GetWebContents(),
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()),
&value)) {
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;
}
VLOG(1) << element_xpath << ", " << get_property_function_body;
return false;
}
// TestRecipeReplayChromeFeatureActionExecutor --------------------------------
TestRecipeReplayChromeFeatureActionExecutor::
TestRecipeReplayChromeFeatureActionExecutor() {}
TestRecipeReplayChromeFeatureActionExecutor::
~TestRecipeReplayChromeFeatureActionExecutor() {}
bool TestRecipeReplayChromeFeatureActionExecutor::AutofillForm(
content::WebContents* web_contents,
const std::string& focus_element_css_selector,
const int attempts) {
return false;
}
} // namespace captured_sites_test_utils
// 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.
#ifndef CHROME_BROWSER_AUTOFILL_CAPTURED_SITES_TEST_UTILS_H_
#define CHROME_BROWSER_AUTOFILL_CAPTURED_SITES_TEST_UTILS_H_
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "chrome/browser/ui/browser.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "services/network/public/cpp/network_switches.h"
namespace captured_sites_test_utils {
// The amount of time to wait for an action to complete, or for a page element
// to appear. The Captured Site Automation Framework uses this timeout to break
// out of wait loops in the event that
// 1. A page load error occurred and the page does not have a page element
// the test expects. Test should stop waiting.
// 2. A page contains persistent animation (such as a flash sale count down
// timer) that causes the framework's paint-based PageActivityObserver to
// wait indefinitely. Test should stop waiting if a sufficiently large time
// has expired for the page to load or for the page to respond to the last
// user action.
const base::TimeDelta default_action_timeout = base::TimeDelta::FromSeconds(30);
std::string FilePathToUTF8(const base::FilePath::StringType& str);
// PageActivityObserver
//
// PageActivityObserver is a universal wait-for-page-ready object that ensures
// the current web page finishes responding to user input. The Captured Site
// Automation Framework, specifically the TestRecipeReplayer class, uses
// PageActivityObserver to time delays between two test actions. Without the
// delay, a test may break itself by performing a page action before the page
// is ready to receive the action.
//
// For example, the Amazon.com checkout page runs background scripts after
// loading. While running the background scripts, the checkout page displays
// a spinner. If a user clicks on a link while the spinner is present,
// Amazon.com will dispatch the user to an error page.
//
// Page readiness is hard to determine because on real-world sites, page
// readiness does not correspond to page load events. In the above Amazon.com
// example, the checkout page starts the background scripts after the page
// finishes loading.
//
// The PageActivityObserver defines page ready as the absence of Chrome paint
// events. On real-world sites, if a page is busy loading, the Chrome tab
// should be busy and Chrome should continuously make layout changes and
// repaint the page. If a site is busy doing background work, most pages
// typically display some form of persistent animation such as a progress bar
// or a spinner to tell the user that the page is not ready. Therefore, it
// is reasonable to assume that a page is ready if Chrome finished painting.
class PageActivityObserver : public content::WebContentsObserver {
public:
explicit PageActivityObserver(content::WebContents* web_contents);
explicit PageActivityObserver(content::RenderFrameHost* frame);
~PageActivityObserver() override = default;
// Wait until Chrome finishes loading a page and updating the page's visuals.
// If Chrome finishes loading a page but continues to paint every half
// second, exit after |continuous_paint_timeout| expires since Chrome
// finished loading the page.
void WaitTillPageIsIdle(
base::TimeDelta continuous_paint_timeout = default_action_timeout);
private:
// PageActivityObserver determines if Chrome stopped painting by checking if
// Chrome hasn't painted for a specific amount of time.
// kPaintEventCheckInterval defines this amount of time.
static constexpr base::TimeDelta kPaintEventCheckInterval =
base::TimeDelta::FromMilliseconds(500);
// content::WebContentsObserver:
void DidCommitAndDrawCompositorFrame() override;
bool paint_occurred_during_last_loop_ = false;
DISALLOW_COPY_AND_ASSIGN(PageActivityObserver);
};
// TestRecipeReplayChromeFeatureActionExecutor
//
// TestRecipeReplayChromeFeatureActionExecutor is a helper interface. A
// TestRecipeReplayChromeFeatureActionExecutor class implements functions
// that automate Chrome feature behavior. TestRecipeReplayer calls
// TestRecipeReplayChromeFeatureActionExecutor functions to execute actions
// that involves a Chrome feature - such as Chrome Autofill or Chrome
// Password Manager. Executing a Chrome feature action typically require
// using private or protected hooks defined inside that feature's
// InProcessBrowserTest class. By implementing this interface an
// InProcessBrowserTest exposes its feature to captured site automation.
class TestRecipeReplayChromeFeatureActionExecutor {
public:
// Chrome Autofill feature methods.
// Triggers Chrome Autofill in the specified input element on the specified
// document.
virtual bool AutofillForm(content::WebContents* web_contents,
const std::string& focus_element_css_selector,
int attempts = 1);
protected:
TestRecipeReplayChromeFeatureActionExecutor();
~TestRecipeReplayChromeFeatureActionExecutor();
DISALLOW_COPY_AND_ASSIGN(TestRecipeReplayChromeFeatureActionExecutor);
};
// TestRecipeReplayer
//
// The TestRecipeReplayer object drives Captured Site Automation by
// 1. Providing a set of functions that help an InProcessBrowserTest to
// configure, start and stop a Web Page Replay (WPR) server. A WPR server
// is a local server that intercepts and responds to Chrome requests with
// pre-recorded traffic. Using a captured site archive file, WPR can
// mimick the site server and provide the test with deterministic site
// behaviors.
// 2. Providing a function that deserializes and replays a Test Recipe. A Test
// Recipe is a JSON formatted file containing instructions on how to run a
// Chrome test against a live or captured site. These instructions include
// the starting URL for the test, and a list of user actions (clicking,
// typing) that drives the test. One may sample some example Test Recipes
// under the src/chrome/test/data/autofill/captured_sites directory.
class TestRecipeReplayer {
public:
static const int kHostHttpPort = 8080;
static const int kHostHttpsPort = 8081;
TestRecipeReplayer(
Browser* browser,
TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor);
~TestRecipeReplayer();
void Setup();
void Cleanup();
// Replay a test by:
// 1. Starting a WPR server using the specified capture file.
// 2. Replaying the specified Test Recipe file.
bool ReplayTest(const base::FilePath capture_file_path,
const base::FilePath recipe_file_path);
static void SetUpCommandLine(base::CommandLine* command_line);
private:
TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor();
content::WebContents* GetWebContents();
void CleanupSiteData();
bool StartWebPageReplayServer(const base::FilePath& capture_file_path);
bool StopWebPageReplayServer();
bool InstallWebPageReplayServerRootCert();
bool RemoveWebPageReplayServerRootCert();
bool RunWebPageReplayCmdAndWaitForExit(
const std::string& cmd,
const std::vector<std::string>& args,
const base::TimeDelta& timeout = base::TimeDelta::FromSeconds(5));
bool RunWebPageReplayCmd(const std::string& cmd,
const std::vector<std::string>& args,
base::Process* process);
bool ReplayRecordedActions(const base::FilePath recipe_file_path);
void InitializeBrowserToExecuteRecipe(
std::unique_ptr<base::DictionaryValue>& recipe);
void ExecuteAutofillAction(base::DictionaryValue* action);
void ExecuteClickAction(base::DictionaryValue* action);
void ExecuteSelectDropdownAction(base::DictionaryValue* action);
void ExecuteTypeAction(base::DictionaryValue* action);
void ExecuteValidateFieldValueAction(base::DictionaryValue* action);
void ExecuteWaitForStateAction(base::DictionaryValue* action);
bool GetTargetHTMLElementXpathFromAction(base::DictionaryValue* action,
std::string* xpath);
void WaitForElemementToBeReady(std::string xpath);
bool WaitForStateChange(
const std::vector<std::string>& state_assertions,
const base::TimeDelta& timeout = default_action_timeout);
bool AllAssertionsPassed(const std::vector<std::string>& assertions);
bool ExecuteJavaScriptOnElementByXpath(
const std::string& element_xpath,
const std::string& execute_function_body,
const base::TimeDelta& time_to_wait_for_element = default_action_timeout);
bool ExpectElementPropertyEquals(
const std::string& element_xpath,
const std::string& get_property_function_body,
const std::string& expected_value,
bool ignoreCase = false);
Browser* browser_;
TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor_;
// The Web Page Replay server that serves the captured sites.
base::Process web_page_replay_server_;
DISALLOW_COPY_AND_ASSIGN(TestRecipeReplayer);
};
} // namespace captured_sites_test_utils
#endif // CHROME_BROWSER_AUTOFILL_CAPTURED_SITES_TEST_UTILS_H_
...@@ -5677,6 +5677,8 @@ if (!is_android && !is_fuchsia) { ...@@ -5677,6 +5677,8 @@ if (!is_android && !is_fuchsia) {
"../browser/autofill/autofill_uitest.h", "../browser/autofill/autofill_uitest.h",
"../browser/autofill/autofill_uitest_util.cc", "../browser/autofill/autofill_uitest_util.cc",
"../browser/autofill/autofill_uitest_util.h", "../browser/autofill/autofill_uitest_util.h",
"../browser/autofill/captured_sites_test_utils.cc",
"../browser/autofill/captured_sites_test_utils.h",
"base/interactive_test_utils.cc", "base/interactive_test_utils.cc",
"base/interactive_test_utils.h", "base/interactive_test_utils.h",
"base/interactive_test_utils_common_views.cc", "base/interactive_test_utils_common_views.cc",
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
{ {
"selector": "//*[@id=\"enterAddressPhoneNumber\"]", "selector": "//*[@id=\"enterAddressPhoneNumber\"]",
"context": [], "context": [],
"expectedAutofillType": "PHONE_HOME_CITY_AND_NUMBER", "expectedAutofillType": "PHONE_HOME_WHOLE_NUMBER",
"expectedValue": "5125551234", "expectedValue": "5125551234",
"type": "validateField" "type": "validateField"
}, },
......
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
"selectorType": "xpath", "selectorType": "xpath",
"selector": "//input[@type='text' and @name='phone']", "selector": "//input[@type='text' and @name='phone']",
"context": [], "context": [],
"expectedAutofillType": "PHONE_HOME_CITY_AND_NUMBER", "expectedAutofillType": "PHONE_HOME_WHOLE_NUMBER",
"expectedValue": "5125551234", "expectedValue": "5125551234",
"type": "validateField" "type": "validateField"
}, },
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
{ {
"selector": "//*[@id=\"AddressForm_PHONE_NUMBER\"]", "selector": "//*[@id=\"AddressForm_PHONE_NUMBER\"]",
"context": [], "context": [],
"expectedAutofillType": "PHONE_HOME_CITY_AND_NUMBER", "expectedAutofillType": "PHONE_HOME_WHOLE_NUMBER",
"expectedValue": "5125551234", "expectedValue": "5125551234",
"type": "validateField" "type": "validateField"
}, },
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment