1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "components/ui_devtools/views/overlay_agent_views.h"
6 
7 #include "components/ui_devtools/ui_devtools_unittest_utils.h"
8 #include "components/ui_devtools/ui_element.h"
9 #include "components/ui_devtools/views/dom_agent_views.h"
10 #include "components/ui_devtools/views/view_element.h"
11 #include "components/ui_devtools/views/widget_element.h"
12 #include "ui/events/base_event_utils.h"
13 #include "ui/events/event_constants.h"
14 #include "ui/events/test/event_generator.h"
15 #include "ui/events/types/event_type.h"
16 #include "ui/gfx/geometry/rect.h"
17 #include "ui/views/test/views_test_base.h"
18 #include "ui/views/widget/widget_utils.h"
19 #include "ui/views/window/non_client_view.h"
20 
21 #if defined(USE_AURA)
22 #include "components/ui_devtools/views/window_element.h"
23 #include "ui/aura/env.h"
24 #include "ui/aura/test/test_window_delegate.h"
25 #include "ui/aura/window.h"
26 #endif
27 
28 namespace ui_devtools {
29 
30 namespace {
31 
GetOriginInScreen(views::View * view)32 gfx::Point GetOriginInScreen(views::View* view) {
33   gfx::Point point(0, 0);  // Since it's local bounds, origin is always 0,0.
34   views::View::ConvertPointToScreen(view, &point);
35   return point;
36 }
37 
38 }  // namespace
39 
40 class OverlayAgentTest : public views::ViewsTestBase {
41  public:
SetUp()42   void SetUp() override {
43     fake_frontend_channel_ = std::make_unique<FakeFrontendChannel>();
44     uber_dispatcher_ = std::make_unique<protocol::UberDispatcher>(
45         fake_frontend_channel_.get());
46     dom_agent_ = DOMAgentViews::Create();
47     dom_agent_->Init(uber_dispatcher_.get());
48     overlay_agent_ = OverlayAgentViews::Create(dom_agent_.get());
49     overlay_agent_->Init(uber_dispatcher_.get());
50     overlay_agent_->enable();
51     views::ViewsTestBase::SetUp();
52   }
53 
TearDown()54   void TearDown() override {
55     // Ensure DOMAgent shuts down before the root window closes to avoid
56     // lifetime issues.
57     overlay_agent_->disable();
58     overlay_agent_.reset();
59     dom_agent_->disable();
60     dom_agent_.reset();
61     uber_dispatcher_.reset();
62     fake_frontend_channel_.reset();
63     widget_.reset();
64     views::ViewsTestBase::TearDown();
65   }
66 
67  protected:
MouseEventAtRootLocation(gfx::Point p)68   std::unique_ptr<ui::MouseEvent> MouseEventAtRootLocation(gfx::Point p) {
69 #if defined(USE_AURA)
70     ui::EventTarget* target = GetContext();
71 #else
72     ui::EventTarget* target = widget()->GetRootView();
73 #endif
74     auto event = std::make_unique<ui::MouseEvent>(ui::ET_MOUSE_MOVED, p, p,
75                                                   ui::EventTimeForNow(),
76                                                   ui::EF_NONE, ui::EF_NONE);
77     ui::Event::DispatcherApi(event.get()).set_target(target);
78     return event;
79   }
80 
GetViewAtPoint(int x,int y)81   views::View* GetViewAtPoint(int x, int y) {
82     gfx::Point point(x, y);
83     int element_id = overlay_agent()->FindElementIdTargetedByPoint(
84         MouseEventAtRootLocation(point).get());
85     UIElement* element = dom_agent()->GetElementFromNodeId(element_id);
86     DCHECK_EQ(element->type(), UIElementType::VIEW);
87     return UIElement::GetBackingElement<views::View, ViewElement>(element);
88   }
GetOverlayNodeHighlightRequestedCount(int node_id)89   int GetOverlayNodeHighlightRequestedCount(int node_id) {
90     return frontend_channel()->CountProtocolNotificationMessage(
91         base::StringPrintf(
92             "{\"method\":\"Overlay.nodeHighlightRequested\",\"params\":{"
93             "\"nodeId\":%d}}",
94             node_id));
95   }
96 
GetOverlayInspectNodeRequestedCount(int node_id)97   int GetOverlayInspectNodeRequestedCount(int node_id) {
98     return frontend_channel()->CountProtocolNotificationMessage(
99         base::StringPrintf(
100             "{\"method\":\"Overlay.inspectNodeRequested\",\"params\":{"
101             "\"backendNodeId\":%d}}",
102             node_id));
103   }
104 
105 #if defined(USE_AURA)
CreateWindowElement(const gfx::Rect & bounds)106   std::unique_ptr<aura::Window> CreateWindowElement(const gfx::Rect& bounds) {
107     std::unique_ptr<aura::Window> window = std::make_unique<aura::Window>(
108         nullptr, aura::client::WINDOW_TYPE_NORMAL);
109     window->Init(ui::LAYER_NOT_DRAWN);
110     window->SetBounds(bounds);
111     GetContext()->AddChild(window.get());
112     window->Show();
113     return window;
114   }
115 #endif
116 
CreateWidget(const gfx::Rect & bounds)117   void CreateWidget(const gfx::Rect& bounds) {
118     widget_ = std::make_unique<views::Widget>();
119     views::Widget::InitParams params;
120     params.delegate = nullptr;
121     params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
122     params.bounds = bounds;
123 #if defined(USE_AURA)
124     params.parent = GetContext();
125 #endif
126     widget_->Init(std::move(params));
127     widget_->Show();
128   }
129 
CreateWidget()130   void CreateWidget() {
131     // Create a widget with default bounds.
132     return CreateWidget(gfx::Rect(0, 0, 400, 400));
133   }
134 
widget()135   views::Widget* widget() { return widget_.get(); }
dom_agent()136   DOMAgentViews* dom_agent() { return dom_agent_.get(); }
overlay_agent()137   OverlayAgentViews* overlay_agent() { return overlay_agent_.get(); }
frontend_channel()138   FakeFrontendChannel* frontend_channel() {
139     return fake_frontend_channel_.get();
140   }
141 
142   std::unique_ptr<protocol::UberDispatcher> uber_dispatcher_;
143   std::unique_ptr<FakeFrontendChannel> fake_frontend_channel_;
144   std::unique_ptr<DOMAgentViews> dom_agent_;
145   std::unique_ptr<OverlayAgentViews> overlay_agent_;
146   std::unique_ptr<views::Widget> widget_;
147 };
148 
149 #if defined(USE_AURA)
TEST_F(OverlayAgentTest,FindElementIdTargetedByPointWindow)150 TEST_F(OverlayAgentTest, FindElementIdTargetedByPointWindow) {
151   //  Windows without delegates won't act as an event handler.
152   aura::test::TestWindowDelegate delegate;
153   std::unique_ptr<aura::Window> window = std::make_unique<aura::Window>(
154       &delegate, aura::client::WINDOW_TYPE_NORMAL);
155   window->Init(ui::LAYER_NOT_DRAWN);
156   window->SetBounds(GetContext()->bounds());
157   GetContext()->AddChild(window.get());
158   window->Show();
159 
160   std::unique_ptr<protocol::DOM::Node> root;
161   dom_agent()->getDocument(&root);
162 
163   int element_id = overlay_agent()->FindElementIdTargetedByPoint(
164       MouseEventAtRootLocation(gfx::Point(1, 1)).get());
165   UIElement* element = dom_agent()->GetElementFromNodeId(element_id);
166   DCHECK_EQ(element->type(), UIElementType::WINDOW);
167   aura::Window* element_window =
168       UIElement::GetBackingElement<aura::Window, WindowElement>(element);
169   EXPECT_EQ(element_window, window.get());
170 
171   gfx::Point out_of_bounds =
172       window->bounds().bottom_right() + gfx::Vector2d(20, 20);
173   EXPECT_EQ(0, overlay_agent()->FindElementIdTargetedByPoint(
174                    MouseEventAtRootLocation(out_of_bounds).get()));
175 }
176 #endif
177 
TEST_F(OverlayAgentTest,FindElementIdTargetedByPointViews)178 TEST_F(OverlayAgentTest, FindElementIdTargetedByPointViews) {
179   CreateWidget();
180 
181   std::unique_ptr<protocol::DOM::Node> root;
182   dom_agent()->getDocument(&root);
183 
184   views::View* contents_view = widget()->GetContentsView();
185   contents_view->RemoveAllChildViews(true);
186 
187   views::View* child_1 = new views::View;
188   views::View* child_2 = new views::View;
189 
190   // Not to scale!
191   // ------------------------
192   // | contents_view        |
193   // |    ----------        |
194   // |    |child_1 |------- |
195   // |    |        |      | |
196   // |    ----------      | |
197   // |            |child_2| |
198   // |            --------- |
199   // |                      |
200   // ------------------------
201   contents_view->AddChildView(child_2);
202   contents_view->AddChildView(child_1);
203   child_1->SetBounds(20, 20, 100, 100);
204   child_2->SetBounds(90, 50, 100, 100);
205 
206   EXPECT_EQ(GetViewAtPoint(1, 1), widget()->GetContentsView());
207   EXPECT_EQ(GetViewAtPoint(21, 21), child_1);
208   EXPECT_EQ(GetViewAtPoint(170, 130), child_2);
209   // At the overlap.
210   EXPECT_EQ(GetViewAtPoint(110, 110), child_1);
211 }
212 
TEST_F(OverlayAgentTest,HighlightRects)213 TEST_F(OverlayAgentTest, HighlightRects) {
214   const struct {
215     std::string name;
216     gfx::Rect first_element_bounds;
217     gfx::Rect second_element_bounds;
218     HighlightRectsConfiguration expected_configuration;
219   } kTestCases[] = {
220       {"R1_CONTAINS_R2", gfx::Rect(1, 1, 100, 100), gfx::Rect(2, 2, 50, 50),
221        R1_CONTAINS_R2},
222       {"R1_HORIZONTAL_FULL_LEFT_R2", gfx::Rect(1, 1, 50, 50),
223        gfx::Rect(60, 1, 60, 60), R1_HORIZONTAL_FULL_LEFT_R2},
224       {"R1_TOP_FULL_LEFT_R2", gfx::Rect(30, 30, 50, 50),
225        gfx::Rect(100, 100, 50, 50), R1_TOP_FULL_LEFT_R2},
226       {"R1_BOTTOM_FULL_LEFT_R2", gfx::Rect(100, 100, 50, 50),
227        gfx::Rect(200, 50, 40, 40), R1_BOTTOM_FULL_LEFT_R2},
228       {"R1_TOP_PARTIAL_LEFT_R2", gfx::Rect(100, 100, 50, 50),
229        gfx::Rect(120, 200, 50, 50), R1_TOP_PARTIAL_LEFT_R2},
230       {"R1_BOTTOM_PARTIAL_LEFT_R2", gfx::Rect(50, 200, 100, 100),
231        gfx::Rect(100, 50, 50, 50), R1_BOTTOM_PARTIAL_LEFT_R2},
232       {"R1_INTERSECTS_R2", gfx::Rect(100, 100, 50, 50),
233        gfx::Rect(120, 120, 50, 50), R1_INTERSECTS_R2},
234   };
235   // Use a non-zero origin to test screen coordinates.
236   const gfx::Rect kWidgetBounds(10, 10, 510, 510);
237 
238   for (const auto& test_case : kTestCases) {
239     SCOPED_TRACE(testing::Message() << "Case: " << test_case.name);
240     CreateWidget(kWidgetBounds);
241     // Can't just use kWidgetBounds because of Mac's menu bar.
242     gfx::Vector2d widget_screen_offset =
243         widget()->GetClientAreaBoundsInScreen().OffsetFromOrigin();
244 
245     std::unique_ptr<protocol::DOM::Node> root;
246     dom_agent()->getDocument(&root);
247 
248     // Fish out the client view to serve as superview. Emptying out the content
249     // view and adding the subviews directly causes NonClientView's hit test to
250     // fail.
251     views::View* contents_view = widget()->GetContentsView();
252     DCHECK_EQ(contents_view->GetClassName(),
253               views::NonClientView::kViewClassName);
254     views::NonClientView* non_client_view =
255         static_cast<views::NonClientView*>(contents_view);
256     views::View* client_view = non_client_view->client_view();
257 
258     views::View* child_1 = new views::View;
259     views::View* child_2 = new views::View;
260     client_view->AddChildView(child_1);
261     client_view->AddChildView(child_2);
262     child_1->SetBoundsRect(test_case.first_element_bounds);
263     child_2->SetBoundsRect(test_case.second_element_bounds);
264 
265     overlay_agent()->setInspectMode(
266         "searchForNode", protocol::Maybe<protocol::Overlay::HighlightConfig>());
267     ui::test::EventGenerator generator(GetRootWindow(widget()));
268     generator.set_assume_window_at_origin(false);
269 
270     // Highlight child 1.
271     generator.MoveMouseTo(GetOriginInScreen(child_1));
272     // Click to pin it.
273     generator.ClickLeftButton();
274     // Highlight child 2. Now, the distance overlay is showing.
275     generator.MoveMouseTo(GetOriginInScreen(child_2));
276 
277     // Check calculated highlight config.
278     EXPECT_EQ(test_case.expected_configuration,
279               overlay_agent()->highlight_rect_config());
280     // Check results of pinned and hovered rectangles.
281     gfx::Rect expected_pinned_rect =
282         client_view->ConvertRectToParent(test_case.first_element_bounds);
283     expected_pinned_rect.Offset(widget_screen_offset);
284     EXPECT_EQ(expected_pinned_rect, overlay_agent()->pinned_rect_);
285     gfx::Rect expected_hovered_rect =
286         client_view->ConvertRectToParent(test_case.second_element_bounds);
287     expected_hovered_rect.Offset(widget_screen_offset);
288     EXPECT_EQ(expected_hovered_rect, overlay_agent()->hovered_rect_);
289     // If we don't explicitly stop inspecting, we'll leave ourselves as
290     // a pretarget handler for the root window and UAF in the next test.
291     // TODO(lgrey): Fix this when refactoring to support Mac.
292     overlay_agent()->setInspectMode(
293         "none", protocol::Maybe<protocol::Overlay::HighlightConfig>());
294   }
295 }
296 
297 // Tests that the correct Overlay events are dispatched to the frontend when
298 // hovering and clicking over a UI element in inspect mode.
TEST_F(OverlayAgentTest,MouseEventsGenerateFEEventsInInspectMode)299 TEST_F(OverlayAgentTest, MouseEventsGenerateFEEventsInInspectMode) {
300   CreateWidget();
301 
302   std::unique_ptr<protocol::DOM::Node> root;
303   dom_agent()->getDocument(&root);
304 
305   gfx::Point p(1, 1);
306   int node_id = overlay_agent()->FindElementIdTargetedByPoint(
307       MouseEventAtRootLocation(p).get());
308 
309   EXPECT_EQ(0, GetOverlayInspectNodeRequestedCount(node_id));
310   EXPECT_EQ(0, GetOverlayNodeHighlightRequestedCount(node_id));
311   overlay_agent()->setInspectMode(
312       "searchForNode", protocol::Maybe<protocol::Overlay::HighlightConfig>());
313 
314   // Moving the mouse cursor over the widget bounds should request a node
315   // highlight.
316   ui::test::EventGenerator generator(GetRootWindow(widget()));
317   generator.MoveMouseBy(p.x(), p.y());
318 
319   // Aura platforms generate both ET_MOUSE_ENTERED and ET_MOUSE_MOVED for
320   // this but Mac just generates ET_MOUSE_ENTERED, so just ensure we sent
321   // at least one.
322   EXPECT_GT(GetOverlayNodeHighlightRequestedCount(node_id), 0);
323   EXPECT_EQ(0, GetOverlayInspectNodeRequestedCount(node_id));
324 
325   // Clicking on the widget should pin that element.
326   generator.ClickLeftButton();
327 
328   // Pin parent node after mouse wheel moves up.
329   int parent_id = dom_agent()->GetParentIdOfNodeId(node_id);
330   EXPECT_NE(parent_id, overlay_agent()->pinned_id());
331   generator.MoveMouseWheel(0, 1);
332   EXPECT_EQ(parent_id, overlay_agent()->pinned_id());
333 
334   // Re-assign pin node.
335   node_id = parent_id;
336 
337   int inspect_node_notification_count =
338       GetOverlayInspectNodeRequestedCount(node_id);
339 
340   // Press escape to exit inspect mode. We're intentionally not supporting
341   // this on Mac due do difficulties in receiving key events without aura::Env.
342 #if defined(USE_AURA)
343   generator.PressKey(ui::KeyboardCode::VKEY_ESCAPE, ui::EventFlags::EF_NONE);
344   // Upon exiting inspect mode, the element is inspected and highlighted.
345   EXPECT_EQ(inspect_node_notification_count + 1,
346             GetOverlayInspectNodeRequestedCount(node_id));
347   ui::Layer* highlighting_layer = overlay_agent()->layer_for_highlighting();
348   const SkColor kBackgroundColor = 0;
349   EXPECT_EQ(kBackgroundColor, highlighting_layer->GetTargetColor());
350   EXPECT_TRUE(highlighting_layer->visible());
351 #else
352   overlay_agent()->setInspectMode(
353       "none", protocol::Maybe<protocol::Overlay::HighlightConfig>());
354 #endif
355 
356   int highlight_notification_count =
357       GetOverlayNodeHighlightRequestedCount(node_id);
358   inspect_node_notification_count =
359       GetOverlayInspectNodeRequestedCount(node_id);
360 
361   // Since inspect mode is exited, a subsequent mouse move should generate no
362   // nodeHighlightRequested or inspectNodeRequested events.
363   generator.MoveMouseBy(p.x(), p.y());
364   EXPECT_EQ(highlight_notification_count,
365             GetOverlayNodeHighlightRequestedCount(node_id));
366   EXPECT_EQ(inspect_node_notification_count,
367             GetOverlayInspectNodeRequestedCount(node_id));
368 }
369 
TEST_F(OverlayAgentTest,HighlightNonexistentNode)370 TEST_F(OverlayAgentTest, HighlightNonexistentNode) {
371   std::unique_ptr<protocol::DOM::Node> root;
372   dom_agent()->getDocument(&root);
373 
374   const int id = 1000;
375   DCHECK(dom_agent()->GetElementFromNodeId(id) == nullptr);
376 
377   overlay_agent()->highlightNode(nullptr, id);
378   if (overlay_agent()->layer_for_highlighting()) {
379     EXPECT_FALSE(overlay_agent()->layer_for_highlighting()->parent());
380     EXPECT_FALSE(overlay_agent()->layer_for_highlighting()->visible());
381   }
382 }
383 
384 #if defined(USE_AURA)
TEST_F(OverlayAgentTest,HighlightWindow)385 TEST_F(OverlayAgentTest, HighlightWindow) {
386   std::unique_ptr<protocol::DOM::Node> root;
387   dom_agent()->getDocument(&root);
388 
389   std::unique_ptr<aura::Window> window =
390       CreateWindowElement(gfx::Rect(0, 0, 20, 20));
391   int window_id =
392       dom_agent()
393           ->element_root()
394           ->FindUIElementIdForBackendElement<aura::Window>(window.get());
395   DCHECK_NE(window_id, 0);
396 
397   overlay_agent()->highlightNode(nullptr, window_id);
398   ui::Layer* highlightingLayer = overlay_agent()->layer_for_highlighting();
399   DCHECK(highlightingLayer);
400 
401   EXPECT_EQ(highlightingLayer->parent(), GetContext()->layer());
402   EXPECT_TRUE(highlightingLayer->visible());
403 
404   overlay_agent()->hideHighlight();
405   EXPECT_FALSE(highlightingLayer->visible());
406 }
407 
TEST_F(OverlayAgentTest,HighlightEmptyOrInvisibleWindow)408 TEST_F(OverlayAgentTest, HighlightEmptyOrInvisibleWindow) {
409   std::unique_ptr<protocol::DOM::Node> root;
410   dom_agent()->getDocument(&root);
411 
412   std::unique_ptr<aura::Window> window = CreateWindowElement(gfx::Rect());
413   int window_id =
414       dom_agent()
415           ->element_root()
416           ->FindUIElementIdForBackendElement<aura::Window>(window.get());
417   DCHECK_NE(window_id, 0);
418 
419   overlay_agent()->highlightNode(nullptr, window_id);
420   ui::Layer* highlightingLayer = overlay_agent()->layer_for_highlighting();
421   DCHECK(highlightingLayer);
422 
423   // Highlight doesn't show for empty element.
424   EXPECT_FALSE(highlightingLayer->parent());
425   EXPECT_FALSE(highlightingLayer->visible());
426 
427   // Make the window non-empty, the highlight shows up.
428   window->SetBounds(gfx::Rect(10, 10, 50, 50));
429   overlay_agent()->highlightNode(nullptr, window_id);
430   EXPECT_EQ(highlightingLayer->parent(), GetContext()->layer());
431   EXPECT_TRUE(highlightingLayer->visible());
432 
433   // Make the window invisible, the highlight still shows.
434   window->Hide();
435   overlay_agent()->highlightNode(nullptr, window_id);
436   EXPECT_EQ(highlightingLayer->parent(), GetContext()->layer());
437   EXPECT_TRUE(highlightingLayer->visible());
438 }
439 #endif
440 
TEST_F(OverlayAgentTest,HighlightWidget)441 TEST_F(OverlayAgentTest, HighlightWidget) {
442   CreateWidget();
443 
444   std::unique_ptr<protocol::DOM::Node> root;
445   dom_agent()->getDocument(&root);
446 
447   int widget_id =
448       dom_agent()
449           ->element_root()
450           ->FindUIElementIdForBackendElement<views::Widget>(widget());
451   DCHECK_NE(widget_id, 0);
452 
453   overlay_agent()->highlightNode(nullptr, widget_id);
454   ui::Layer* highlightingLayer = overlay_agent()->layer_for_highlighting();
455   DCHECK(highlightingLayer);
456 
457 #if defined(USE_AURA)
458   EXPECT_EQ(highlightingLayer->parent(), GetContext()->layer());
459 #else
460 // TODO(https://crbug.com/898280): Fix this for Mac.
461 #endif
462   EXPECT_TRUE(highlightingLayer->visible());
463 
464   overlay_agent()->hideHighlight();
465   EXPECT_FALSE(highlightingLayer->visible());
466 }
467 
468 }  // namespace ui_devtools
469