1// Copyright 2017 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 "ui/base/cocoa/bubble_closer.h"
6
7#include "base/bind.h"
8#import "ui/base/test/cocoa_helper.h"
9#import "ui/base/test/menu_test_observer.h"
10#import "ui/events/test/cocoa_test_event_utils.h"
11
12namespace ui {
13
14class BubbleCloserTest : public CocoaTest {
15 public:
16  enum Button { LEFT, RIGHT };
17  enum InOrOut { INSIDE, OUTSIDE };
18
19  BubbleCloserTest() = default;
20
21  void SetUp() override {
22    CocoaTest::SetUp();
23    bubble_window_.reset([[NSWindow alloc]
24        initWithContentRect:NSMakeRect(100, 100, 320, 200)
25                  styleMask:NSBorderlessWindowMask
26                    backing:NSBackingStoreBuffered
27                      defer:NO]);
28    [bubble_window_ setReleasedWhenClosed:NO];  // Owned by scoped_nsobject.
29    [bubble_window_ makeKeyAndOrderFront:nil];
30    bubble_closer_ = std::make_unique<BubbleCloser>(
31        bubble_window_,
32        base::BindRepeating([](int* i) { *i += 1; }, &click_outside_count_));
33  }
34
35  void TearDown() override {
36    [bubble_window_ close];
37    bubble_closer_ = nullptr;
38    bubble_window_.reset();
39    CocoaTest::TearDown();
40  }
41
42  void SendClick(Button left_or_right, InOrOut in_or_out) {
43    NSWindow* window = in_or_out == INSIDE ? bubble_window_ : test_window();
44    NSEvent* event =
45        left_or_right == LEFT
46            ? cocoa_test_event_utils::LeftMouseDownAtPointInWindow(
47                  NSMakePoint(10, 10), window)
48            : cocoa_test_event_utils::RightMouseDownAtPointInWindow(
49                  NSMakePoint(10, 10), window);
50    [NSApp sendEvent:event];
51  }
52
53  void ResetCloser() { bubble_closer_ = nullptr; }
54
55 protected:
56  int click_outside_count() const { return click_outside_count_; }
57  NSWindow* bubble_window() { return bubble_window_; }
58  bool IsCloserReset() const { return !bubble_closer_; }
59
60 private:
61  base::scoped_nsobject<NSWindow> bubble_window_;
62  std::unique_ptr<BubbleCloser> bubble_closer_;
63  int click_outside_count_ = 0;
64
65  DISALLOW_COPY_AND_ASSIGN(BubbleCloserTest);
66};
67
68// Test for lifetime issues around NSEvent monitors.
69TEST_F(BubbleCloserTest, SecondBubbleCloser) {
70  auto resetter = [](BubbleCloserTest* me) { me->ResetCloser(); };
71  auto deleter = std::make_unique<BubbleCloser>(
72      bubble_window(), base::BindRepeating(resetter, this));
73  SendClick(LEFT, OUTSIDE);
74
75  // The order is non-deterministic, so click_outside_count() may not change.
76  // But either way, the closer should have been reset.
77  EXPECT_TRUE(IsCloserReset());
78}
79
80// Test that clicking outside the window fires the callback and clicking inside
81// does not.
82TEST_F(BubbleCloserTest, ClickInsideAndOut) {
83  EXPECT_EQ(0, click_outside_count());
84  SendClick(LEFT, OUTSIDE);
85  EXPECT_EQ(1, click_outside_count());
86  SendClick(RIGHT, OUTSIDE);
87  EXPECT_EQ(2, click_outside_count());
88  SendClick(LEFT, INSIDE);
89  EXPECT_EQ(2, click_outside_count());
90  SendClick(RIGHT, INSIDE);
91  EXPECT_EQ(2, click_outside_count());
92}
93
94// Test that right-clicking the window to display a context menu works.
95TEST_F(BubbleCloserTest, RightClickOutsideClosesWithContextMenu) {
96  base::scoped_nsobject<NSMenu> context_menu(
97      [[NSMenu alloc] initWithTitle:@""]);
98  [context_menu addItemWithTitle:@"ContextMenuTest"
99                          action:nil
100                   keyEquivalent:@""];
101
102  // Set the menu as the contextual menu of contentView of test_window().
103  [[test_window() contentView] setMenu:context_menu];
104
105  base::scoped_nsobject<MenuTestObserver> menu_observer(
106      [[MenuTestObserver alloc] initWithMenu:context_menu]);
107  [menu_observer setCloseAfterOpening:YES];
108  [menu_observer setOpenCallback:^(MenuTestObserver* observer) {
109    // Verify click is seen when contextual menu is open.
110    EXPECT_TRUE([observer isOpen]);
111    EXPECT_EQ(1, click_outside_count());
112  }];
113
114  EXPECT_FALSE([menu_observer isOpen]);
115  EXPECT_FALSE([menu_observer didOpen]);
116
117  EXPECT_EQ(0, click_outside_count());
118
119  // RightMouseDown in test_window() would close the bubble window and then
120  // display the contextual menu.
121  SendClick(RIGHT, OUTSIDE);
122
123  // When we got here, menu has already run its RunLoop.
124  EXPECT_EQ(1, click_outside_count());
125
126  EXPECT_FALSE([menu_observer isOpen]);
127  EXPECT_TRUE([menu_observer didOpen]);
128}
129
130}  // namespace ui
131