blob: c03005bfde144f48b46bc2120d89840a28a7f5e1 [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 "components/autofill_assistant/browser/script_executor.h"
#include <map>
#include <utility>
#include "base/strings/strcat.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_task_environment.h"
#include "components/autofill_assistant/browser/client_memory.h"
#include "components/autofill_assistant/browser/mock_run_once_callback.h"
#include "components/autofill_assistant/browser/mock_service.h"
#include "components/autofill_assistant/browser/mock_ui_controller.h"
#include "components/autofill_assistant/browser/mock_web_controller.h"
#include "components/autofill_assistant/browser/service.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace autofill_assistant {
namespace {
using ::testing::_;
using ::testing::AllOf;
using ::testing::Contains;
using ::testing::DoAll;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Field;
using ::testing::Invoke;
using ::testing::IsEmpty;
using ::testing::NiceMock;
using ::testing::Not;
using ::testing::Pair;
using ::testing::Property;
using ::testing::ReturnRef;
using ::testing::SaveArg;
using ::testing::SizeIs;
using ::testing::StrEq;
using ::testing::StrictMock;
const char* kScriptPath = "script_path";
class ScriptExecutorTest : public testing::Test,
public ScriptExecutorDelegate,
public ScriptExecutor::Listener {
public:
void SetUp() override {
executor_ = std::make_unique<ScriptExecutor>(
kScriptPath,
/* global_payload= */ "initial global payload",
/* script_payload= */ "initial payload",
/* listener= */ this, &scripts_state_, &ordered_interrupts_,
/* delegate= */ this);
url_ = GURL("http://example.com/");
// In this test, "tell" actions always succeed and "click" actions always
// fail. The following makes a click action fail immediately
ON_CALL(mock_web_controller_, OnClickOrTapElement(_, _))
.WillByDefault(RunOnceCallback<1>(false));
ON_CALL(mock_web_controller_, OnElementCheck(_, _, _))
.WillByDefault(RunOnceCallback<2>(true));
ON_CALL(mock_web_controller_, OnFocusElement(_, _))
.WillByDefault(RunOnceCallback<1>(true));
ON_CALL(mock_web_controller_, GetUrl()).WillByDefault(ReturnRef(url_));
}
protected:
ScriptExecutorTest()
: scoped_task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME) {}
// Implements ScriptExecutorDelegate
Service* GetService() override { return &mock_service_; }
UiController* GetUiController() override { return &mock_ui_controller_; }
WebController* GetWebController() override { return &mock_web_controller_; }
ClientMemory* GetClientMemory() override { return &memory_; }
void SetTouchableElementArea(const ElementAreaProto& area) {}
void SetStatusMessage(const std::string& status_message) {
status_message_ = status_message;
}
std::string GetStatusMessage() const override { return status_message_; }
void ClearDetails() override { cleared_details_ = true; }
void SetDetails(const Details& details) override {}
void EnterState(AutofillAssistantState state) {}
const std::map<std::string, std::string>& GetParameters() override {
return parameters_;
}
autofill::PersonalDataManager* GetPersonalDataManager() override {
return nullptr;
}
content::WebContents* GetWebContents() override { return nullptr; }
// Implements ScriptExecutor::Listener
void OnServerPayloadChanged(const std::string& global_payload,
const std::string& script_payload) override {
last_global_payload_ = global_payload;
last_script_payload_ = script_payload;
}
void OnScriptListChanged(
std::vector<std::unique_ptr<Script>> scripts) override {
should_update_scripts_ = true;
scripts_update_ = std::move(scripts);
++scripts_update_count_;
}
std::string Serialize(const google::protobuf::MessageLite& message) {
std::string output;
message.SerializeToString(&output);
return output;
}
// Creates a script that contains a wait_for_dom allow_interrupt=true followed
// by a tell. It will succeed if |element| eventually becomes visible.
void SetupInterruptibleScript(const std::string& path,
const std::string& element) {
ActionsResponseProto interruptible;
interruptible.set_global_payload("main script global payload");
interruptible.set_script_payload("main script payload");
auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom();
wait_action->add_selectors(element);
wait_action->set_allow_interrupt(true);
interruptible.add_actions()->mutable_tell()->set_message(path);
EXPECT_CALL(mock_service_, OnGetActions(StrEq(path), _, _, _, _, _))
.WillRepeatedly(RunOnceCallback<5>(true, Serialize(interruptible)));
}
// Creates an interrupt that contains a tell. It will always succeed.
void SetupInterrupt(const std::string& path, const std::string& trigger) {
RegisterInterrupt(path, trigger);
ActionsResponseProto interrupt_actions;
InitInterruptActions(&interrupt_actions, path);
EXPECT_CALL(mock_service_, OnGetActions(StrEq(path), _, _, _, _, _))
.WillRepeatedly(RunOnceCallback<5>(true, Serialize(interrupt_actions)));
}
void InitInterruptActions(ActionsResponseProto* interrupt_actions,
const std::string& path) {
interrupt_actions->set_global_payload(
base::StrCat({"global payload for ", path}));
interrupt_actions->set_script_payload(base::StrCat({"payload for ", path}));
interrupt_actions->add_actions()->mutable_tell()->set_message(path);
}
// Registers an interrupt, but do not define actions for it.
void RegisterInterrupt(const std::string& path, const std::string& trigger) {
auto interrupt = std::make_unique<Script>();
interrupt->handle.path = path;
ScriptPreconditionProto interrupt_preconditions;
interrupt_preconditions.add_elements_exist()->add_selectors(trigger);
interrupt->precondition =
ScriptPrecondition::FromProto(path, interrupt_preconditions);
ordered_interrupts_.push_back(interrupt.get());
interrupts_.emplace_back(std::move(interrupt));
}
// scoped_task_environment_ must be first to guarantee other field
// creation run in that environment.
base::test::ScopedTaskEnvironment scoped_task_environment_;
Script script_;
ClientMemory memory_;
StrictMock<MockService> mock_service_;
NiceMock<MockWebController> mock_web_controller_;
NiceMock<MockUiController> mock_ui_controller_;
std::map<std::string, ScriptStatusProto> scripts_state_;
// An owner for the pointers in |ordered_interrupts_|
std::vector<std::unique_ptr<Script>> interrupts_;
std::vector<Script*> ordered_interrupts_;
std::string last_global_payload_;
std::string last_script_payload_;
bool should_update_scripts_ = false;
std::vector<std::unique_ptr<Script>> scripts_update_;
int scripts_update_count_ = 0;
std::unique_ptr<ScriptExecutor> executor_;
std::map<std::string, std::string> parameters_;
StrictMock<base::MockCallback<ScriptExecutor::RunScriptCallback>>
executor_callback_;
GURL url_;
std::string status_message_;
bool cleared_details_ = false;
};
TEST_F(ScriptExecutorTest, GetActionsFails) {
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(false, ""));
EXPECT_CALL(executor_callback_,
Run(AllOf(Field(&ScriptExecutor::Result::success, false),
Field(&ScriptExecutor::Result::at_end,
ScriptExecutor::CONTINUE))));
executor_->Run(executor_callback_.Get());
}
TEST_F(ScriptExecutorTest, ForwardParameters) {
parameters_["param1"] = "value1";
parameters_["param2"] = "value2";
EXPECT_CALL(mock_service_,
OnGetActions(StrEq(kScriptPath), _,
AllOf(Contains(Pair("param1", "value1")),
Contains(Pair("param2", "value2"))),
_, _, _))
.WillOnce(RunOnceCallback<5>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
}
TEST_F(ScriptExecutorTest, RunOneActionReportAndReturn) {
ActionsResponseProto actions_response;
actions_response.add_actions()
->mutable_click()
->mutable_element_to_click()
->add_selectors("will fail");
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
std::vector<ProcessedActionProto> processed_actions_capture;
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(DoAll(SaveArg<2>(&processed_actions_capture),
RunOnceCallback<3>(true, "")));
EXPECT_CALL(executor_callback_,
Run(AllOf(Field(&ScriptExecutor::Result::success, true),
Field(&ScriptExecutor::Result::at_end,
ScriptExecutor::CONTINUE))));
executor_->Run(executor_callback_.Get());
ASSERT_EQ(1u, processed_actions_capture.size());
EXPECT_EQ(OTHER_ACTION_STATUS, processed_actions_capture[0].status());
}
TEST_F(ScriptExecutorTest, RunMultipleActions) {
ActionsResponseProto initial_actions_response;
initial_actions_response.add_actions()->mutable_tell()->set_message("1");
initial_actions_response.add_actions()->mutable_tell()->set_message("2");
EXPECT_CALL(mock_service_, OnGetActions(StrEq(kScriptPath), _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(initial_actions_response)));
ActionsResponseProto next_actions_response;
next_actions_response.add_actions()->mutable_tell()->set_message("3");
std::vector<ProcessedActionProto> processed_actions1_capture;
std::vector<ProcessedActionProto> processed_actions2_capture;
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(
DoAll(SaveArg<2>(&processed_actions1_capture),
RunOnceCallback<3>(true, Serialize(next_actions_response))))
.WillOnce(DoAll(SaveArg<2>(&processed_actions2_capture),
RunOnceCallback<3>(true, "")));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_EQ(2u, processed_actions1_capture.size());
EXPECT_EQ(1u, processed_actions2_capture.size());
}
TEST_F(ScriptExecutorTest, UnsupportedAction) {
ActionsResponseProto actions_response;
actions_response.add_actions(); // action definition missing
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
std::vector<ProcessedActionProto> processed_actions_capture;
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(DoAll(SaveArg<2>(&processed_actions_capture),
RunOnceCallback<3>(true, "")));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
ASSERT_EQ(1u, processed_actions_capture.size());
EXPECT_EQ(UNSUPPORTED_ACTION, processed_actions_capture[0].status());
}
TEST_F(ScriptExecutorTest, StopAfterEnd) {
ActionsResponseProto actions_response;
actions_response.add_actions()->mutable_stop();
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(AllOf(Field(&ScriptExecutor::Result::success, true),
Field(&ScriptExecutor::Result::at_end,
ScriptExecutor::SHUTDOWN))));
executor_->Run(executor_callback_.Get());
}
TEST_F(ScriptExecutorTest, ResetAfterEnd) {
ActionsResponseProto actions_response;
actions_response.add_actions()->mutable_reset();
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(AllOf(Field(&ScriptExecutor::Result::success, true),
Field(&ScriptExecutor::Result::at_end,
ScriptExecutor::RESTART))));
executor_->Run(executor_callback_.Get());
}
TEST_F(ScriptExecutorTest, InterruptActionListOnError) {
ActionsResponseProto initial_actions_response;
initial_actions_response.add_actions()->mutable_tell()->set_message(
"will pass");
initial_actions_response.add_actions()
->mutable_click()
->mutable_element_to_click()
->add_selectors("will fail");
initial_actions_response.add_actions()->mutable_tell()->set_message(
"never run");
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(initial_actions_response)));
ActionsResponseProto next_actions_response;
next_actions_response.add_actions()->mutable_tell()->set_message(
"will run after error");
std::vector<ProcessedActionProto> processed_actions1_capture;
std::vector<ProcessedActionProto> processed_actions2_capture;
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(
DoAll(SaveArg<2>(&processed_actions1_capture),
RunOnceCallback<3>(true, Serialize(next_actions_response))))
.WillOnce(DoAll(SaveArg<2>(&processed_actions2_capture),
RunOnceCallback<3>(true, "")));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
ASSERT_EQ(2u, processed_actions1_capture.size());
EXPECT_EQ(ACTION_APPLIED, processed_actions1_capture[0].status());
EXPECT_EQ(OTHER_ACTION_STATUS, processed_actions1_capture[1].status());
ASSERT_EQ(1u, processed_actions2_capture.size());
EXPECT_EQ(ACTION_APPLIED, processed_actions2_capture[0].status());
// make sure "never run" wasn't the one that was run.
EXPECT_EQ("will run after error",
processed_actions2_capture[0].action().tell().message());
}
TEST_F(ScriptExecutorTest, RunDelayedAction) {
ActionsResponseProto actions_response;
ActionProto* action = actions_response.add_actions();
action->mutable_tell()->set_message("delayed");
action->set_action_delay_ms(1000);
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
std::vector<ProcessedActionProto> processed_actions_capture;
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(DoAll(SaveArg<2>(&processed_actions_capture),
RunOnceCallback<3>(true, "")));
// executor_callback_.Run() not expected to be run just yet, as the action is
// delayed.
executor_->Run(executor_callback_.Get());
EXPECT_TRUE(scoped_task_environment_.MainThreadHasPendingTask());
// Moving forward in time triggers action execution.
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
scoped_task_environment_.FastForwardBy(
base::TimeDelta::FromMilliseconds(1000));
EXPECT_FALSE(scoped_task_environment_.MainThreadHasPendingTask());
}
TEST_F(ScriptExecutorTest, ClearDetailsWhenFinished) {
ActionsResponseProto actions_response;
ActionProto click_with_clean_contextual_ui;
click_with_clean_contextual_ui.set_clean_contextual_ui(true);
click_with_clean_contextual_ui.mutable_tell()->set_message("clean");
*actions_response.add_actions() = click_with_clean_contextual_ui;
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_TRUE(cleared_details_);
}
TEST_F(ScriptExecutorTest, DontClearDetailsIfOtherActionsAreLeft) {
ActionsResponseProto actions_response;
ActionProto click_with_clean_contextual_ui;
click_with_clean_contextual_ui.set_clean_contextual_ui(true);
click_with_clean_contextual_ui.mutable_tell()->set_message("clean");
*actions_response.add_actions() = click_with_clean_contextual_ui;
actions_response.add_actions()->mutable_tell()->set_message("Wait no!");
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_FALSE(cleared_details_);
}
TEST_F(ScriptExecutorTest, ClearDetailsOnError) {
ActionsResponseProto actions_response;
actions_response.add_actions()->mutable_tell()->set_message("Hello");
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(false, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, false)));
executor_->Run(executor_callback_.Get());
EXPECT_TRUE(cleared_details_);
}
TEST_F(ScriptExecutorTest, UpdateScriptStateWhileRunning) {
// OnGetNextActions never calls the callback, so Run() returns immediately
// without doing anything.
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _));
EXPECT_THAT(scripts_state_, IsEmpty());
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_RUNNING)));
}
TEST_F(ScriptExecutorTest, UpdateScriptStateOnError) {
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(false, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, false)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_FAILURE)));
}
TEST_F(ScriptExecutorTest, UpdateScriptStateOnSuccess) {
ActionsResponseProto initial_actions_response;
initial_actions_response.add_actions()->mutable_tell()->set_message("ok");
EXPECT_CALL(mock_service_, OnGetActions(StrEq(kScriptPath), _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(initial_actions_response)));
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_SUCCESS)));
}
TEST_F(ScriptExecutorTest, ForwardLastPayloadOnSuccess) {
ActionsResponseProto actions_response;
actions_response.set_global_payload("actions global payload");
actions_response.set_script_payload("actions payload");
actions_response.add_actions()->mutable_tell()->set_message("ok");
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, "initial global payload",
"initial payload", _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
ActionsResponseProto next_actions_response;
next_actions_response.set_global_payload("last global payload");
next_actions_response.set_script_payload("last payload");
EXPECT_CALL(mock_service_, OnGetNextActions("actions global payload",
"actions payload", _, _))
.WillOnce(RunOnceCallback<3>(true, Serialize(next_actions_response)));
EXPECT_CALL(executor_callback_, Run(_));
executor_->Run(executor_callback_.Get());
EXPECT_EQ("last global payload", last_global_payload_);
EXPECT_EQ("last payload", last_script_payload_);
}
TEST_F(ScriptExecutorTest, ForwardLastPayloadOnError) {
ActionsResponseProto actions_response;
actions_response.set_global_payload("actions global payload");
actions_response.set_script_payload("actions payload");
actions_response.add_actions()->mutable_tell()->set_message("ok");
EXPECT_CALL(mock_service_, OnGetActions(_, _, _, "initial global payload",
"initial payload", _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
EXPECT_CALL(mock_service_, OnGetNextActions("actions global payload",
"actions payload", _, _))
.WillOnce(RunOnceCallback<3>(false, ""));
EXPECT_CALL(executor_callback_, Run(_));
executor_->Run(executor_callback_.Get());
EXPECT_EQ("actions global payload", last_global_payload_);
EXPECT_EQ("actions payload", last_script_payload_);
}
TEST_F(ScriptExecutorTest, RunInterrupt) {
// All elements exist, so first the interrupt should be run, then the element
// should be reported as found.
SetupInterruptibleScript(kScriptPath, "element");
SetupInterrupt("interrupt", "interrupt_trigger");
// Both scripts ends after the first set of actions. Capture the results.
std::vector<ProcessedActionProto> processed_actions1_capture;
std::vector<ProcessedActionProto> processed_actions2_capture;
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(DoAll(SaveArg<2>(&processed_actions1_capture),
RunOnceCallback<3>(true, "")))
.WillOnce(DoAll(SaveArg<2>(&processed_actions2_capture),
RunOnceCallback<3>(true, "")));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_SUCCESS)));
EXPECT_THAT(scripts_state_,
Contains(Pair("interrupt", SCRIPT_STATUS_SUCCESS)));
// The first script to call OnGetNextActions is the interrupt, which starts
// with a tell.
ASSERT_THAT(processed_actions1_capture, Not(IsEmpty()));
EXPECT_EQ(ActionProto::ActionInfoCase::kTell,
processed_actions1_capture[0].action().action_info_case());
EXPECT_EQ(ACTION_APPLIED, processed_actions1_capture[0].status());
// The second script to call OnGetNextActions is the main script, with starts
// with a wait_for_dom
ASSERT_THAT(processed_actions2_capture, Not(IsEmpty()));
EXPECT_EQ(ActionProto::ActionInfoCase::kWaitForDom,
processed_actions2_capture[0].action().action_info_case());
EXPECT_EQ(ACTION_APPLIED, processed_actions2_capture[0].status());
}
TEST_F(ScriptExecutorTest, RunMultipleInterruptInOrder) {
// All elements exist. The two interrupts should run, in order, then the
// element should be reported as found.
SetupInterruptibleScript(kScriptPath, "element");
SetupInterrupt("interrupt1", "interrupt_trigger1");
SetupInterrupt("interrupt2", "interrupt_trigger2");
{
testing::InSequence seq;
EXPECT_CALL(mock_service_,
OnGetNextActions(_, "payload for interrupt1", _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(mock_service_,
OnGetNextActions(_, "payload for interrupt2", _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(mock_service_, OnGetNextActions(_, "main script payload", _, _))
.WillOnce(RunOnceCallback<3>(true, ""));
}
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_SUCCESS)));
EXPECT_THAT(scripts_state_,
Contains(Pair("interrupt1", SCRIPT_STATUS_SUCCESS)));
EXPECT_THAT(scripts_state_,
Contains(Pair("interrupt2", SCRIPT_STATUS_SUCCESS)));
}
TEST_F(ScriptExecutorTest, RunSameInterruptMultipleTimes) {
// In a main script with three wait_for_dom with allow_interrupt=true...
ActionsResponseProto interruptible;
for (int i = 0; i < 3; i++) {
auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom();
wait_action->add_selectors("element");
wait_action->set_allow_interrupt(true);
}
EXPECT_CALL(mock_service_, OnGetActions(StrEq("script_path"), _, _, _, _, _))
.WillRepeatedly(RunOnceCallback<5>(true, Serialize(interruptible)));
// 'interrupt' with matching preconditions runs exactly three times.
RegisterInterrupt("interrupt", "interrupt_trigger");
ActionsResponseProto interrupt_actions;
InitInterruptActions(&interrupt_actions, "interrupt");
EXPECT_CALL(mock_service_, OnGetActions(StrEq("interrupt"), _, _, _, _, _))
.Times(3)
.WillRepeatedly(RunOnceCallback<5>(true, Serialize(interrupt_actions)));
// All scripts succeed with no more actions.
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillRepeatedly(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
}
TEST_F(ScriptExecutorTest, ForwardMainScriptPayloadWhenInterruptRuns) {
SetupInterruptibleScript(kScriptPath, "element");
SetupInterrupt("interrupt", "interrupt_trigger");
ActionsResponseProto next_interrupt_actions_response;
next_interrupt_actions_response.set_global_payload(
"last global payload from interrupt");
next_interrupt_actions_response.set_script_payload(
"last payload from interrupt");
EXPECT_CALL(mock_service_, OnGetNextActions("global payload for interrupt",
"payload for interrupt", _, _))
.WillOnce(
RunOnceCallback<3>(true, Serialize(next_interrupt_actions_response)));
ActionsResponseProto next_main_actions_response;
next_main_actions_response.set_global_payload(
"last global payload from main");
next_main_actions_response.set_script_payload("last payload from main");
EXPECT_CALL(mock_service_,
OnGetNextActions("last global payload from interrupt",
"main script payload", _, _))
.WillOnce(
RunOnceCallback<3>(true, Serialize(next_main_actions_response)));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_EQ("last global payload from main", last_global_payload_);
EXPECT_EQ("last payload from main", last_script_payload_);
}
TEST_F(ScriptExecutorTest, ForwardMainScriptPayloadWhenInterruptFails) {
SetupInterruptibleScript(kScriptPath, "element");
SetupInterrupt("interrupt", "interrupt_trigger");
EXPECT_CALL(mock_service_, OnGetNextActions("global payload for interrupt",
"payload for interrupt", _, _))
.WillOnce(RunOnceCallback<3>(false, ""));
EXPECT_CALL(mock_service_, OnGetNextActions("global payload for interrupt",
"main script payload", _, _))
.WillOnce(RunOnceCallback<3>(false, ""));
EXPECT_CALL(executor_callback_, Run(_));
executor_->Run(executor_callback_.Get());
EXPECT_EQ("global payload for interrupt", last_global_payload_);
EXPECT_EQ("main script payload", last_script_payload_);
}
TEST_F(ScriptExecutorTest, DoNotRunInterruptIfPreconditionsDontMatch) {
// interrupt_trigger does not exist, but element does, so wait_for_dom will
// succeed without calling the interrupt.
SetupInterruptibleScript(kScriptPath, "element");
SetupInterrupt("interrupt", "interrupt_trigger");
EXPECT_CALL(mock_web_controller_,
OnElementCheck(_, Eq(Selector({"element"})), _))
.WillRepeatedly(RunOnceCallback<2>(true));
EXPECT_CALL(mock_web_controller_,
OnElementCheck(_, Eq(Selector({"interrupt_trigger"})), _))
.WillRepeatedly(RunOnceCallback<2>(false));
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillRepeatedly(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_SUCCESS)));
EXPECT_THAT(scripts_state_, Not(Contains(Pair(StrEq("interrupt"), _))));
}
TEST_F(ScriptExecutorTest, DoNotRunInterruptIfNotInterruptible) {
// The main script has a wait_for_dom, but it is not interruptible.
ActionsResponseProto interruptible;
auto* wait_action = interruptible.add_actions()->mutable_wait_for_dom();
wait_action->add_selectors("element");
// allow_interrupt is not set
EXPECT_CALL(mock_service_, OnGetActions(StrEq(kScriptPath), _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(interruptible)));
// The interrupt would trigger, since interrupt_trigger exits, but it's not
// given an opportunity to.
SetupInterrupt("interrupt", "interrupt_trigger");
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillRepeatedly(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_SUCCESS)));
EXPECT_THAT(scripts_state_, Not(Contains(Pair(StrEq("interrupt"), _))));
}
TEST_F(ScriptExecutorTest, InterruptFailsMainScript) {
// The interrupt is run and fails. Failure should cascade.
SetupInterruptibleScript(kScriptPath, "element");
SetupInterrupt("interrupt", "interrupt_trigger");
// The interrupt fails.
EXPECT_CALL(mock_service_, OnGetNextActions(_, "payload for interrupt", _, _))
.WillOnce(RunOnceCallback<3>(false, ""));
// The main script gets a report of the failure from the interrupt, and fails
// in turn.
EXPECT_CALL(
mock_service_,
OnGetNextActions(
_, "main script payload",
ElementsAre(Property(&ProcessedActionProto::status,
ProcessedActionStatusProto::INTERRUPT_FAILED)),
_))
.WillOnce(RunOnceCallback<3>(false, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, false)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_FAILURE)));
EXPECT_THAT(scripts_state_,
Contains(Pair("interrupt", SCRIPT_STATUS_FAILURE)));
}
TEST_F(ScriptExecutorTest, InterruptReturnsShutdown) {
// The interrupt succeeds, but executes the stop action. This should stop the
// execution of the main script and make it return result.at_end=SHUTDOWN
SetupInterruptibleScript(kScriptPath, "element");
RegisterInterrupt("interrupt", "interrupt_trigger");
ActionsResponseProto interrupt_actions;
interrupt_actions.add_actions()->mutable_stop();
// Get interrupt actions
EXPECT_CALL(mock_service_, OnGetActions(StrEq("interrupt"), _, _, _, _, _))
.WillRepeatedly(RunOnceCallback<5>(true, Serialize(interrupt_actions)));
// We expect to get result of interrupt action, then result of the main script
// action.
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.Times(2)
.WillRepeatedly(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(AllOf(Field(&ScriptExecutor::Result::success, true),
Field(&ScriptExecutor::Result::at_end,
ScriptExecutor::SHUTDOWN))));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_SUCCESS)));
EXPECT_THAT(scripts_state_,
Contains(Pair("interrupt", SCRIPT_STATUS_SUCCESS)));
}
TEST_F(ScriptExecutorTest, UpdateScriptListGetNext) {
should_update_scripts_ = false;
scripts_update_.clear();
scripts_update_count_ = 0;
ActionsResponseProto initial_actions_response;
initial_actions_response.add_actions()->mutable_tell()->set_message("1");
EXPECT_CALL(mock_service_, OnGetActions(StrEq(kScriptPath), _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(initial_actions_response)));
ActionsResponseProto next_actions_response;
next_actions_response.add_actions()->mutable_tell()->set_message("2");
auto* script =
next_actions_response.mutable_update_script_list()->add_scripts();
script->set_path("path");
auto* presentation = script->mutable_presentation();
presentation->set_name("name");
presentation->mutable_precondition();
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(true, Serialize(next_actions_response)))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_TRUE(should_update_scripts_);
EXPECT_THAT(scripts_update_, SizeIs(1));
EXPECT_THAT(scripts_update_count_, Eq(1));
EXPECT_THAT("path", scripts_update_[0]->handle.path);
EXPECT_THAT("name", scripts_update_[0]->handle.name);
}
TEST_F(ScriptExecutorTest, UpdateScriptListShouldNotifyMultipleTimes) {
should_update_scripts_ = false;
scripts_update_.clear();
scripts_update_count_ = 0;
ActionsResponseProto actions_response;
actions_response.add_actions()->mutable_tell()->set_message("hi");
auto* script = actions_response.mutable_update_script_list()->add_scripts();
script->set_path("path");
auto* presentation = script->mutable_presentation();
presentation->set_name("name");
presentation->mutable_precondition();
EXPECT_CALL(mock_service_, OnGetActions(StrEq(kScriptPath), _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(actions_response)));
script->set_path("path2");
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.WillOnce(RunOnceCallback<3>(true, Serialize(actions_response)))
.WillOnce(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_TRUE(should_update_scripts_);
EXPECT_THAT(scripts_update_count_, Eq(2));
EXPECT_THAT(scripts_update_, SizeIs(1));
EXPECT_THAT("path2", scripts_update_[0]->handle.path);
}
TEST_F(ScriptExecutorTest, UpdateScriptListFromInterrupt) {
should_update_scripts_ = false;
scripts_update_.clear();
scripts_update_count_ = 0;
SetupInterruptibleScript(kScriptPath, "element");
RegisterInterrupt("interrupt", "interrupt_trigger");
ActionsResponseProto interrupt_actions;
interrupt_actions.add_actions()->mutable_tell()->set_message("abc");
EXPECT_CALL(mock_service_, OnGetActions(StrEq("interrupt"), _, _, _, _, _))
.WillOnce(RunOnceCallback<5>(true, Serialize(interrupt_actions)));
auto* script = interrupt_actions.mutable_update_script_list()->add_scripts();
script->set_path("path");
auto* presentation = script->mutable_presentation();
presentation->set_name("update_from_interrupt");
presentation->mutable_precondition();
// We expect a call from the interrupt which will update the script list and a
// second call from the interrupt to terminate. Then a call from the main
// script which will finish without running any actions.
EXPECT_CALL(mock_service_, OnGetNextActions(_, _, _, _))
.Times(3)
.WillOnce(RunOnceCallback<3>(true, Serialize(interrupt_actions)))
.WillRepeatedly(RunOnceCallback<3>(true, ""));
EXPECT_CALL(executor_callback_,
Run(Field(&ScriptExecutor::Result::success, true)));
executor_->Run(executor_callback_.Get());
EXPECT_THAT(scripts_state_,
Contains(Pair(kScriptPath, SCRIPT_STATUS_SUCCESS)));
EXPECT_THAT(scripts_state_,
Contains(Pair("interrupt", SCRIPT_STATUS_SUCCESS)));
EXPECT_TRUE(should_update_scripts_);
EXPECT_THAT(scripts_update_, SizeIs(1));
EXPECT_THAT(scripts_update_count_, Eq(1));
EXPECT_THAT("path", scripts_update_[0]->handle.path);
EXPECT_THAT("update_from_interrupt", scripts_update_[0]->handle.name);
}
} // namespace
} // namespace autofill_assistant