Commit 97e11262 authored by Clemens Arbesser's avatar Clemens Arbesser Committed by Commit Bot

[Autofill Assistant] Implemented dynamic trigger conditions.

The intended use case is for a single instance of this class to be
shared with all trigger scripts. The combined set of selector conditions
will be extracted and is then repeatedly evaluated as necessary. Trigger
scripts will simply access the cached results, rather than invoke their
computation themselves.

This keeps the actual element lookup separate from the trigger scripts,
and should also be much more efficient for the common use case where two
trigger scripts only differ by is_first_time_user or some other static
trigger condition.

Bug: b/171776026
Change-Id: I48fad77e32abdfbf39f9414d159b31047b6fba32
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2514422
Commit-Queue: Clemens Arbesser <arbesser@google.com>
Reviewed-by: default avatarMarian Fechete <marianfe@google.com>
Cr-Commit-Position: refs/heads/master@{#823580}
parent c247ff5a
...@@ -349,6 +349,7 @@ source_set("unit_tests") { ...@@ -349,6 +349,7 @@ source_set("unit_tests") {
"test_util.cc", "test_util.cc",
"test_util.h", "test_util.h",
"trigger_context_unittest.cc", "trigger_context_unittest.cc",
"trigger_scripts/dynamic_trigger_conditions_unittest.cc",
"trigger_scripts/trigger_script_coordinator_unittest.cc", "trigger_scripts/trigger_script_coordinator_unittest.cc",
"trigger_scripts/trigger_script_unittest.cc", "trigger_scripts/trigger_script_unittest.cc",
"user_data_util_unittest.cc", "user_data_util_unittest.cc",
......
...@@ -4,18 +4,93 @@ ...@@ -4,18 +4,93 @@
#include "components/autofill_assistant/browser/trigger_scripts/dynamic_trigger_conditions.h" #include "components/autofill_assistant/browser/trigger_scripts/dynamic_trigger_conditions.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
namespace autofill_assistant { namespace autofill_assistant {
namespace {
// Extracts all selectors from the condition tree in |proto| and writes them to
// |results|.
void ExtractSelectors(const TriggerScriptConditionProto& proto,
std::set<Selector>* results) {
switch (proto.type_case()) {
case TriggerScriptConditionProto::kAllOf:
for (const auto& condition : proto.all_of().conditions()) {
ExtractSelectors(condition, results);
}
return;
case TriggerScriptConditionProto::kAnyOf:
for (const auto& condition : proto.any_of().conditions()) {
ExtractSelectors(condition, results);
}
return;
case TriggerScriptConditionProto::kNoneOf:
for (const auto& condition : proto.none_of().conditions()) {
ExtractSelectors(condition, results);
}
return;
case TriggerScriptConditionProto::kStoredLoginCredentials:
case TriggerScriptConditionProto::kIsFirstTimeUser:
case TriggerScriptConditionProto::kExperimentId:
case TriggerScriptConditionProto::TYPE_NOT_SET:
return;
case TriggerScriptConditionProto::kSelector:
results->insert(Selector(proto.selector()));
return;
}
}
} // namespace
DynamicTriggerConditions::DynamicTriggerConditions() = default; DynamicTriggerConditions::DynamicTriggerConditions() = default;
DynamicTriggerConditions::~DynamicTriggerConditions() = default; DynamicTriggerConditions::~DynamicTriggerConditions() = default;
void DynamicTriggerConditions::AddSelectorsFromTriggerScript(
const TriggerScriptProto& proto) {
ExtractSelectors(proto.trigger_condition(), &selectors_);
}
base::Optional<bool> DynamicTriggerConditions::GetSelectorMatches( base::Optional<bool> DynamicTriggerConditions::GetSelectorMatches(
const Selector& selector) const { const Selector& selector) const {
return false; auto it = selector_matches_.find(selector);
if (it == selector_matches_.end()) {
return base::nullopt;
}
return it->second;
} }
void DynamicTriggerConditions::Update(WebController* web_controller, void DynamicTriggerConditions::Update(WebController* web_controller,
base::OnceCallback<void(void)> callback) { base::OnceCallback<void(void)> callback) {
DCHECK(!callback_) << "Update called while already in progress";
if (callback_) {
return;
}
selector_matches_.clear();
if (selectors_.empty()) {
std::move(callback).Run();
return;
}
callback_ = std::move(callback);
for (const auto& selector : selectors_) {
web_controller->FindElement(
selector, /* strict = */ true,
base::BindOnce(&DynamicTriggerConditions::OnFindElement,
weak_ptr_factory_.GetWeakPtr(), selector));
}
}
void DynamicTriggerConditions::OnFindElement(
const Selector& selector,
const ClientStatus& client_status,
std::unique_ptr<ElementFinder::Result> element) {
selector_matches_.emplace(std::make_pair(selector, client_status.ok()));
if (selector_matches_.size() == selectors_.size()) {
std::move(callback_).Run();
}
} }
} // namespace autofill_assistant } // namespace autofill_assistant
...@@ -5,7 +5,13 @@ ...@@ -5,7 +5,13 @@
#ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_TRIGGER_SCRIPTS_DYNAMIC_TRIGGER_CONDITIONS_H_ #ifndef COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_TRIGGER_SCRIPTS_DYNAMIC_TRIGGER_CONDITIONS_H_
#define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_TRIGGER_SCRIPTS_DYNAMIC_TRIGGER_CONDITIONS_H_ #define COMPONENTS_AUTOFILL_ASSISTANT_BROWSER_TRIGGER_SCRIPTS_DYNAMIC_TRIGGER_CONDITIONS_H_
#include <map>
#include <set>
#include "base/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h" #include "base/optional.h"
#include "components/autofill_assistant/browser/selector.h"
#include "components/autofill_assistant/browser/web/web_controller.h" #include "components/autofill_assistant/browser/web/web_controller.h"
namespace autofill_assistant { namespace autofill_assistant {
...@@ -18,12 +24,44 @@ class DynamicTriggerConditions { ...@@ -18,12 +24,44 @@ class DynamicTriggerConditions {
DynamicTriggerConditions(); DynamicTriggerConditions();
~DynamicTriggerConditions(); ~DynamicTriggerConditions();
// TODO(b/171776026): implement this stub. // Adds the selector trigger conditions specified in |proto| to the list of
// selectors to be queried in |Update|.
virtual void AddSelectorsFromTriggerScript(const TriggerScriptProto& proto);
// Returns whether |selector| currently matches the DOM tree. |Update| must
// be called prior to this method. Only selectors that have previously been
// added via |AddSelectorsFromTriggerScript| can be queried.
virtual base::Optional<bool> GetSelectorMatches( virtual base::Optional<bool> GetSelectorMatches(
const Selector& selector) const; const Selector& selector) const;
// Matches all previously added selectors with the current DOM tree and caches
// the results to be available via |GetSelectorMatches|. Invokes |callback|
// when done.
//
// NOTE: this class is not thread-safe. Don't invoke any of its methods while
// |Update| is running.
virtual void Update(WebController* web_controller, virtual void Update(WebController* web_controller,
base::OnceCallback<void(void)> callback); base::OnceCallback<void(void)> callback);
private:
friend class DynamicTriggerConditionsTest;
// Writes the result of the element lookup to |selector_matches_|. When all
// |selectors_| have been evaluated, i.e., when the size of
// |selector_matches_| is equal to the size of |selectors_|, invokes
// |callback_|.
void OnFindElement(const Selector& selector,
const ClientStatus& client_status,
std::unique_ptr<ElementFinder::Result> element);
// Lookup cache for selector matches. Must be updated by invoking |Update|.
std::map<Selector, bool> selector_matches_;
// The list of selectors to look up on |Update|.
std::set<Selector> selectors_;
// The callback to invoke after |Update| is finished. Only set during
// |Update|.
base::OnceCallback<void(void)> callback_;
base::WeakPtrFactory<DynamicTriggerConditions> weak_ptr_factory_{this};
}; };
} // namespace autofill_assistant } // namespace autofill_assistant
......
// Copyright 2020 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/trigger_scripts/trigger_script_coordinator.h"
#include "components/autofill_assistant/browser/selector.h"
#include "components/autofill_assistant/browser/web/mock_web_controller.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/test/bind_test_util.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gtest_util.h"
#include "base/test/mock_callback.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace autofill_assistant {
using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::UnorderedElementsAre;
class DynamicTriggerConditionsTest : public testing::Test {
public:
DynamicTriggerConditionsTest() = default;
~DynamicTriggerConditionsTest() override = default;
protected:
std::set<Selector>* GetSelectorsForTest() {
return &dynamic_trigger_conditions_.selectors_;
}
base::MockCallback<base::OnceCallback<void(void)>> mock_callback_;
DynamicTriggerConditions dynamic_trigger_conditions_;
NiceMock<MockWebController> mock_web_controller_;
};
TEST_F(DynamicTriggerConditionsTest, UpdateWithoutSelectorsDoesNothing) {
EXPECT_CALL(mock_web_controller_, OnFindElement).Times(0);
EXPECT_CALL(mock_callback_, Run).Times(1);
dynamic_trigger_conditions_.Update(&mock_web_controller_,
mock_callback_.Get());
}
TEST_F(DynamicTriggerConditionsTest, LookupInvalidSelectorsFails) {
EXPECT_EQ(dynamic_trigger_conditions_.GetSelectorMatches(
Selector(ToSelectorProto("not_evaluated"))),
base::nullopt);
}
TEST_F(DynamicTriggerConditionsTest, AddSelectorsFromTriggerScript) {
TriggerScriptProto proto_1;
auto* all_of = proto_1.mutable_trigger_condition()->mutable_all_of();
*all_of->add_conditions()->mutable_selector() = ToSelectorProto("a");
auto* any_of = all_of->add_conditions()->mutable_any_of();
*any_of->add_conditions()->mutable_selector() = ToSelectorProto("b");
auto* none_of = any_of->add_conditions()->mutable_none_of();
*none_of->add_conditions()->mutable_selector() = ToSelectorProto("c");
TriggerScriptProto proto_2;
*proto_2.mutable_trigger_condition()->mutable_selector() =
ToSelectorProto("d");
TriggerScriptProto proto_3;
all_of = proto_3.mutable_trigger_condition()->mutable_all_of();
*all_of->add_conditions()->mutable_selector() = ToSelectorProto("a");
*all_of->add_conditions()->mutable_selector() = ToSelectorProto("e");
*all_of->add_conditions()->mutable_selector() = ToSelectorProto("f");
dynamic_trigger_conditions_.AddSelectorsFromTriggerScript(proto_1);
dynamic_trigger_conditions_.AddSelectorsFromTriggerScript(proto_2);
dynamic_trigger_conditions_.AddSelectorsFromTriggerScript(proto_3);
EXPECT_THAT(
*GetSelectorsForTest(),
UnorderedElementsAre(
Selector(ToSelectorProto("a")), Selector(ToSelectorProto("b")),
Selector(ToSelectorProto("c")), Selector(ToSelectorProto("d")),
Selector(ToSelectorProto("e")), Selector(ToSelectorProto("f"))));
}
TEST_F(DynamicTriggerConditionsTest, Update) {
TriggerScriptProto proto;
auto* all_of = proto.mutable_trigger_condition()->mutable_all_of();
*all_of->add_conditions()->mutable_selector() = ToSelectorProto("a");
*all_of->add_conditions()->mutable_selector() = ToSelectorProto("b");
*all_of->add_conditions()->mutable_selector() = ToSelectorProto("c");
EXPECT_CALL(mock_web_controller_,
OnFindElement(Selector(ToSelectorProto("a")), _))
.WillOnce(RunOnceCallback<1>(OkClientStatus(), nullptr));
EXPECT_CALL(mock_web_controller_,
OnFindElement(Selector(ToSelectorProto("b")), _))
.WillOnce(
RunOnceCallback<1>(ClientStatus(ELEMENT_RESOLUTION_FAILED), nullptr));
EXPECT_CALL(mock_web_controller_,
OnFindElement(Selector(ToSelectorProto("c")), _))
.WillOnce(RunOnceCallback<1>(OkClientStatus(), nullptr));
EXPECT_CALL(mock_callback_, Run).Times(1);
dynamic_trigger_conditions_.AddSelectorsFromTriggerScript(proto);
dynamic_trigger_conditions_.Update(&mock_web_controller_,
mock_callback_.Get());
EXPECT_EQ(dynamic_trigger_conditions_.GetSelectorMatches(
Selector(ToSelectorProto("a"))),
base::make_optional(true));
EXPECT_EQ(dynamic_trigger_conditions_.GetSelectorMatches(
Selector(ToSelectorProto("b"))),
base::make_optional(false));
EXPECT_EQ(dynamic_trigger_conditions_.GetSelectorMatches(
Selector(ToSelectorProto("c"))),
base::make_optional(true));
}
} // namespace autofill_assistant
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