1 // Copyright 2014 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.
5 #import "ui/views/controls/menu/menu_runner_impl_cocoa.h"
7 #import <Cocoa/Cocoa.h>
9 #include "base/strings/utf_string_conversions.h"
10 #import "testing/gtest_mac.h"
11 #include "ui/base/models/simple_menu_model.h"
12 #include "ui/events/event_utils.h"
13 #include "ui/views/test/views_test_base.h"
19 class TestModel : public ui::SimpleMenuModel {
21 TestModel() : ui::SimpleMenuModel(&delegate_), delegate_(this) {}
23 void set_checked_command(int command) { checked_command_ = command; }
26 class Delegate : public ui::SimpleMenuModel::Delegate {
28 explicit Delegate(TestModel* model) : model_(model) {}
29 bool IsCommandIdChecked(int command_id) const override {
30 return command_id == model_->checked_command_;
32 bool IsCommandIdEnabled(int command_id) const override { return true; }
33 bool GetAcceleratorForCommandId(int command_id,
34 ui::Accelerator* accelerator) override {
37 void ExecuteCommand(int command_id, int event_flags) override {}
42 DISALLOW_COPY_AND_ASSIGN(Delegate);
46 int checked_command_ = -1;
49 DISALLOW_COPY_AND_ASSIGN(TestModel);
54 class MenuRunnerCocoaTest : public ViewsTestBase {
56 MenuRunnerCocoaTest() {}
57 ~MenuRunnerCocoaTest() override {}
59 void SetUp() override {
60 ViewsTestBase::SetUp();
62 menu_.reset(new TestModel());
63 menu_->AddCheckItem(0, base::ASCIIToUTF16("Menu Item"));
65 runner_ = new internal::MenuRunnerImplCocoa(menu_.get());
66 EXPECT_FALSE(runner_->IsRunning());
69 void TearDown() override {
75 ViewsTestBase::TearDown();
78 // Runs the menu after scheduling |block| on the run loop.
79 MenuRunner::RunResult RunMenu(dispatch_block_t block) {
80 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
81 EXPECT_TRUE(runner_->IsRunning());
84 return runner_->RunMenuAt(
85 NULL, NULL, gfx::Rect(), MENU_ANCHOR_TOPLEFT, MenuRunner::CONTEXT_MENU);
88 // Runs then cancels a combobox menu and captures the frame of the anchoring
90 MenuRunner::RunResult RunMenuAt(const gfx::Rect& anchor) {
91 last_anchor_frame_ = NSZeroRect;
93 // Should be one child (the compositor layer) before showing, and it should
94 // go up by one (the anchor view) while the menu is shown.
95 EXPECT_EQ(1u, [[parent_->GetNativeView() subviews] count]);
96 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
97 NSArray* subviews = [parent_->GetNativeView() subviews];
98 EXPECT_EQ(2u, [subviews count]);
99 last_anchor_frame_ = [[subviews objectAtIndex:1] frame];
102 MenuRunner::RunResult result = runner_->RunMenuAt(
103 parent_, nullptr, anchor, MENU_ANCHOR_TOPLEFT, MenuRunner::COMBOBOX);
105 // Ensure the anchor view is removed.
106 EXPECT_EQ(1u, [[parent_->GetNativeView() subviews] count]);
111 scoped_ptr<TestModel> menu_;
112 internal::MenuRunnerImplCocoa* runner_ = nullptr;
113 views::Widget* parent_ = nullptr;
114 NSRect last_anchor_frame_ = NSZeroRect;
117 DISALLOW_COPY_AND_ASSIGN(MenuRunnerCocoaTest);
120 TEST_F(MenuRunnerCocoaTest, RunMenuAndCancel) {
121 base::TimeDelta min_time = ui::EventTimeForNow();
123 MenuRunner::RunResult result = RunMenu(^{
125 EXPECT_FALSE(runner_->IsRunning());
128 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
129 EXPECT_FALSE(runner_->IsRunning());
131 EXPECT_GE(runner_->GetClosingEventTime(), min_time);
132 EXPECT_LE(runner_->GetClosingEventTime(), ui::EventTimeForNow());
136 EXPECT_FALSE(runner_->IsRunning());
139 TEST_F(MenuRunnerCocoaTest, RunMenuAndDelete) {
140 MenuRunner::RunResult result = RunMenu(^{
145 EXPECT_EQ(MenuRunner::MENU_DELETED, result);
148 TEST_F(MenuRunnerCocoaTest, RunMenuTwice) {
149 for (int i = 0; i < 2; ++i) {
150 MenuRunner::RunResult result = RunMenu(^{
153 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result);
154 EXPECT_FALSE(runner_->IsRunning());
158 TEST_F(MenuRunnerCocoaTest, CancelWithoutRunning) {
160 EXPECT_FALSE(runner_->IsRunning());
161 EXPECT_EQ(base::TimeDelta(), runner_->GetClosingEventTime());
164 TEST_F(MenuRunnerCocoaTest, DeleteWithoutRunning) {
169 // Tests anchoring of the menus used for toolkit-views Comboboxes.
170 TEST_F(MenuRunnerCocoaTest, ComboboxAnchoring) {
171 const int kWindowHeight = 200;
172 const int kWindowOffset = 100;
174 parent_ = new views::Widget();
175 parent_->Init(CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS));
177 gfx::Rect(kWindowOffset, kWindowOffset, 300, kWindowHeight));
180 // Combobox at 20,10 in the Widget.
181 const gfx::Rect combobox_rect(20, 10, 80, 50);
183 // Menu anchor rects are always in screen coordinates. The window is frameless
184 // so offset by the bounds.
185 gfx::Rect anchor_rect = combobox_rect;
186 anchor_rect.Offset(kWindowOffset, kWindowOffset);
187 RunMenuAt(anchor_rect);
189 // Nothing is checked, so the anchor view should have no height, to ensure the
190 // menu goes below the anchor rect. There should also be no x-offset since the
191 // there is no need to line-up text.
193 NSMakeRect(combobox_rect.x(), kWindowHeight - combobox_rect.bottom(),
194 combobox_rect.width(), 0),
197 menu_->set_checked_command(0);
198 RunMenuAt(anchor_rect);
200 // Native constant used by MenuRunnerImplCocoa.
201 const CGFloat kNativeCheckmarkWidth = 18;
203 // There is now a checked item, so the anchor should be vertically centered
204 // inside the combobox, and offset by the width of the checkmark column.
205 EXPECT_EQ(combobox_rect.x() - kNativeCheckmarkWidth,
206 last_anchor_frame_.origin.x);
207 EXPECT_EQ(kWindowHeight - combobox_rect.CenterPoint().y(),
208 NSMidY(last_anchor_frame_));
209 EXPECT_EQ(combobox_rect.width(), NSWidth(last_anchor_frame_));
210 EXPECT_NE(0, NSHeight(last_anchor_frame_));
212 // In RTL, Cocoa messes up the positioning unless the anchor rectangle is
213 // offset to the right of the view. The offset for the checkmark is also
214 // skipped, to give a better match to native behavior.
215 base::i18n::SetICUDefaultLocale("he");
216 RunMenuAt(anchor_rect);
217 EXPECT_EQ(combobox_rect.right(), last_anchor_frame_.origin.x);