| // 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/element_area.h" |
| |
| #include <algorithm> |
| #include <map> |
| #include <ostream> |
| |
| #include "base/bind.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "components/autofill_assistant/browser/fake_script_executor_delegate.h" |
| #include "components/autofill_assistant/browser/mock_run_once_callback.h" |
| #include "components/autofill_assistant/browser/mock_web_controller.h" |
| #include "components/autofill_assistant/browser/script_executor_delegate.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| |
| using ::testing::_; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::IsEmpty; |
| |
| namespace autofill_assistant { |
| |
| // User-friendly RectF string representation for matchers. |
| // |
| // operator<< must not be in an anonymous namespace to be usable in all |
| // matchers. |
| std::string ToString(const RectF& rect) { |
| return base::StringPrintf("RectF(%2.2f, %2.2f, %2.2f, %2.2f)", rect.left, |
| rect.top, rect.right, rect.bottom); |
| } |
| |
| std::ostream& operator<<(std::ostream& out, const RectF& rectf) { |
| return out << ToString(rectf); |
| } |
| |
| namespace { |
| |
| MATCHER_P4(MatchingRectF, |
| left, |
| top, |
| right, |
| bottom, |
| ToString(RectF{left, top, right, bottom})) { |
| if (abs(left - arg.left) < 0.01 && abs(top - arg.top) < 0.01 && |
| abs(right - arg.right) < 0.01 && abs(bottom - arg.bottom) < 0.01) { |
| return true; |
| } |
| *result_listener << arg; |
| return false; |
| } |
| |
| MATCHER(EmptyRectF, "EmptyRectF") { |
| if (arg.empty()) |
| return true; |
| |
| *result_listener << arg; |
| return false; |
| } |
| |
| ACTION(DoNothing) {} |
| |
| class ElementAreaTest : public testing::Test { |
| protected: |
| ElementAreaTest() |
| : scoped_task_environment_( |
| base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME), |
| element_area_(&delegate_) { |
| delegate_.SetWebController(&mock_web_controller_); |
| delegate_.GetMutableSettings()->element_position_update_interval = |
| base::TimeDelta::FromMilliseconds(100); |
| |
| ON_CALL(mock_web_controller_, OnGetElementPosition(_, _)) |
| .WillByDefault(RunOnceCallback<1>(false, RectF())); |
| ON_CALL(mock_web_controller_, OnGetVisualViewport(_)) |
| .WillByDefault(RunOnceCallback<0>(true, RectF(0, 0, 200, 400))); |
| |
| element_area_.SetOnUpdate(base::BindRepeating(&ElementAreaTest::OnUpdate, |
| base::Unretained(this))); |
| } |
| |
| void SetElement(const std::string& selector) { |
| ElementAreaProto area; |
| area.add_rectangles()->add_elements()->add_selectors(selector); |
| element_area_.SetFromProto(area); |
| } |
| |
| void OnUpdate(const RectF& visual_viewport, const std::vector<RectF>& area) { |
| on_update_call_count_++; |
| reported_visual_viewport_ = visual_viewport; |
| reported_area_ = area; |
| } |
| |
| // scoped_task_environment_ must be first to guarantee other field |
| // creation run in that environment. |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| |
| MockWebController mock_web_controller_; |
| FakeScriptExecutorDelegate delegate_; |
| ElementArea element_area_; |
| int on_update_call_count_ = 0; |
| RectF reported_visual_viewport_; |
| std::vector<RectF> reported_area_; |
| }; |
| |
| TEST_F(ElementAreaTest, Empty) { |
| EXPECT_THAT(reported_area_, IsEmpty()); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, IsEmpty()); |
| |
| RectF viewport; |
| element_area_.GetVisualViewport(&viewport); |
| EXPECT_THAT(viewport, EmptyRectF()); |
| } |
| |
| TEST_F(ElementAreaTest, ElementNotFound) { |
| SetElement("#not_found"); |
| EXPECT_THAT(reported_area_, ElementsAre(EmptyRectF())); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(EmptyRectF())); |
| } |
| |
| TEST_F(ElementAreaTest, GetVisualViewport) { |
| SetElement("#some_element"); |
| RectF viewport; |
| element_area_.GetVisualViewport(&viewport); |
| EXPECT_THAT(viewport, MatchingRectF(0, 0, 200, 400)); |
| } |
| |
| TEST_F(ElementAreaTest, OneRectangle) { |
| EXPECT_CALL(mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#found"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(25, 25, 75, 75))); |
| |
| SetElement("#found"); |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(25, 25, 75, 75))); |
| } |
| |
| TEST_F(ElementAreaTest, CallOnUpdate) { |
| EXPECT_CALL(mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#found"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(25, 25, 75, 75))); |
| |
| SetElement("#found"); |
| EXPECT_EQ(on_update_call_count_, 1); |
| EXPECT_THAT(reported_visual_viewport_, MatchingRectF(0, 0, 200, 400)); |
| EXPECT_THAT(reported_area_, ElementsAre(MatchingRectF(25, 25, 75, 75))); |
| } |
| |
| TEST_F(ElementAreaTest, DontCallOnUpdateWhenViewportMissing) { |
| // Swallowing calls to OnGetVisualViewport guarantees that the viewport |
| // position will never be known. |
| EXPECT_CALL(mock_web_controller_, OnGetVisualViewport(_)) |
| .WillOnce(DoNothing()); |
| EXPECT_CALL(mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#found"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(25, 25, 75, 75))); |
| |
| SetElement("#found"); |
| EXPECT_EQ(on_update_call_count_, 0); |
| } |
| |
| TEST_F(ElementAreaTest, CallOnUpdateWhenViewportMissingAndEmptyRect) { |
| EXPECT_CALL(mock_web_controller_, OnGetVisualViewport(_)) |
| .WillRepeatedly(RunOnceCallback<0>(false, RectF())); |
| |
| SetElement("#found"); |
| |
| // A newly empty element area should be reported. |
| on_update_call_count_ = 0; |
| element_area_.Clear(); |
| |
| EXPECT_EQ(on_update_call_count_, 1); |
| EXPECT_THAT(reported_visual_viewport_, EmptyRectF()); |
| EXPECT_THAT(reported_area_, IsEmpty()); |
| } |
| |
| TEST_F(ElementAreaTest, TwoRectangles) { |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#top_left"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(0, 0, 25, 25))); |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#bottom_right"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(25, 25, 100, 100))); |
| |
| ElementAreaProto area_proto; |
| area_proto.add_rectangles()->add_elements()->add_selectors("#top_left"); |
| area_proto.add_rectangles()->add_elements()->add_selectors("#bottom_right"); |
| element_area_.SetFromProto(area_proto); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(0, 0, 25, 25), |
| MatchingRectF(25, 25, 100, 100))); |
| } |
| |
| TEST_F(ElementAreaTest, OneRectangleTwoElements) { |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element1"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(1, 3, 2, 4))); |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element2"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(5, 2, 6, 5))); |
| |
| ElementAreaProto area_proto; |
| auto* rectangle_proto = area_proto.add_rectangles(); |
| rectangle_proto->add_elements()->add_selectors("#element1"); |
| rectangle_proto->add_elements()->add_selectors("#element2"); |
| element_area_.SetFromProto(area_proto); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(1, 2, 6, 5))); |
| } |
| |
| TEST_F(ElementAreaTest, DoNotReportIncompleteRectangles) { |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element1"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(1, 3, 2, 4))); |
| |
| // Getting the position of #element2 neither succeeds nor fails, simulating an |
| // intermediate state which shouldn't be reported to the callback. |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element2"}).MustBeVisible()), _)) |
| .WillOnce(DoNothing()); // overrides default action |
| |
| ElementAreaProto area_proto; |
| auto* rectangle_proto = area_proto.add_rectangles(); |
| rectangle_proto->add_elements()->add_selectors("#element1"); |
| rectangle_proto->add_elements()->add_selectors("#element2"); |
| element_area_.SetFromProto(area_proto); |
| |
| EXPECT_THAT(reported_area_, IsEmpty()); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(1, 3, 2, 4))); |
| } |
| |
| TEST_F(ElementAreaTest, OneRectangleFourElements) { |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element1"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(0, 0, 1, 1))); |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element2"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(9, 9, 100, 100))); |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element3"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(0, 9, 1, 100))); |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element4"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(9, 0, 100, 1))); |
| |
| ElementAreaProto area_proto; |
| auto* rectangle_proto = area_proto.add_rectangles(); |
| rectangle_proto->add_elements()->add_selectors("#element1"); |
| rectangle_proto->add_elements()->add_selectors("#element2"); |
| rectangle_proto->add_elements()->add_selectors("#element3"); |
| rectangle_proto->add_elements()->add_selectors("#element4"); |
| element_area_.SetFromProto(area_proto); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(0, 0, 100, 100))); |
| } |
| |
| TEST_F(ElementAreaTest, OneRectangleMissingElementsReported) { |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element1"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(1, 1, 2, 2))); |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element2"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(false, RectF())); |
| |
| ElementAreaProto area_proto; |
| auto* rectangle_proto = area_proto.add_rectangles(); |
| rectangle_proto->add_elements()->add_selectors("#element1"); |
| rectangle_proto->add_elements()->add_selectors("#element2"); |
| element_area_.SetFromProto(area_proto); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(1, 1, 2, 2))); |
| |
| EXPECT_THAT(reported_area_, ElementsAre(MatchingRectF(1, 1, 2, 2))); |
| } |
| |
| TEST_F(ElementAreaTest, FullWidthRectangle) { |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element1"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(1, 3, 2, 4))); |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element2"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(5, 7, 6, 8))); |
| EXPECT_CALL(mock_web_controller_, OnGetVisualViewport(_)) |
| .WillRepeatedly(RunOnceCallback<0>(true, RectF(100, 0, 200, 400))); |
| |
| ElementAreaProto area_proto; |
| auto* rectangle_proto = area_proto.add_rectangles(); |
| rectangle_proto->add_elements()->add_selectors("#element1"); |
| rectangle_proto->add_elements()->add_selectors("#element2"); |
| rectangle_proto->set_full_width(true); |
| element_area_.SetFromProto(area_proto); |
| |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| |
| // left and right of the box come from the visual viewport, top from the 1st |
| // element, bottom from the 2nd. |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(100, 3, 200, 8))); |
| } |
| |
| TEST_F(ElementAreaTest, ElementMovesAfterUpdate) { |
| testing::InSequence seq; |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(0, 25, 100, 50))) |
| .WillOnce(RunOnceCallback<1>(true, RectF(0, 50, 100, 75))); |
| |
| SetElement("#element"); |
| |
| std::vector<RectF> original; |
| element_area_.GetRectangles(&original); |
| EXPECT_THAT(original, ElementsAre(MatchingRectF(0, 25, 100, 50))); |
| EXPECT_THAT(reported_area_, ElementsAre(MatchingRectF(0, 25, 100, 50))); |
| |
| element_area_.Update(); |
| |
| // Updated area is available |
| std::vector<RectF> updated; |
| element_area_.GetRectangles(&updated); |
| EXPECT_THAT(updated, ElementsAre(MatchingRectF(0, 50, 100, 75))); |
| |
| // Updated area is reported |
| EXPECT_THAT(reported_area_, ElementsAre(MatchingRectF(0, 50, 100, 75))); |
| } |
| |
| TEST_F(ElementAreaTest, ElementMovesWithTime) { |
| testing::InSequence seq; |
| EXPECT_CALL( |
| mock_web_controller_, |
| OnGetElementPosition(Eq(Selector({"#element"}).MustBeVisible()), _)) |
| .WillOnce(RunOnceCallback<1>(true, RectF(0, 25, 100, 50))) |
| .WillRepeatedly(RunOnceCallback<1>(true, RectF(0, 50, 100, 75))); |
| |
| SetElement("#element"); |
| |
| EXPECT_THAT(reported_area_, ElementsAre(MatchingRectF(0, 25, 100, 50))); |
| |
| scoped_task_environment_.FastForwardBy( |
| base::TimeDelta::FromMilliseconds(100)); |
| |
| // Updated area is available |
| std::vector<RectF> rectangles; |
| element_area_.GetRectangles(&rectangles); |
| EXPECT_THAT(rectangles, ElementsAre(MatchingRectF(0, 50, 100, 75))); |
| |
| // Updated area is reported |
| EXPECT_THAT(reported_area_, ElementsAre(MatchingRectF(0, 50, 100, 75))); |
| } |
| } // namespace |
| } // namespace autofill_assistant |