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