| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #import "ui/views/controls/menu/menu_runner_impl_cocoa.h" | 5 #import "ui/views/controls/menu/menu_runner_impl_cocoa.h" |
| 6 | 6 |
| 7 #import <Cocoa/Cocoa.h> | 7 #import <Cocoa/Cocoa.h> |
| 8 | 8 |
| 9 #include "base/macros.h" | 9 #include "base/macros.h" |
| 10 #include "base/run_loop.h" |
| 10 #include "base/strings/utf_string_conversions.h" | 11 #include "base/strings/utf_string_conversions.h" |
| 12 #include "base/test/test_timeouts.h" |
| 13 #include "base/threading/thread_task_runner_handle.h" |
| 11 #import "testing/gtest_mac.h" | 14 #import "testing/gtest_mac.h" |
| 12 #include "ui/base/models/simple_menu_model.h" | 15 #include "ui/base/models/simple_menu_model.h" |
| 13 #include "ui/events/event_utils.h" | 16 #include "ui/events/event_utils.h" |
| 17 #include "ui/views/controls/menu/menu_runner_impl_adapter.h" |
| 14 #include "ui/views/test/views_test_base.h" | 18 #include "ui/views/test/views_test_base.h" |
| 15 | 19 |
| 16 namespace views { | 20 namespace views { |
| 17 namespace test { | 21 namespace test { |
| 18 namespace { | 22 namespace { |
| 19 | 23 |
| 20 class TestModel : public ui::SimpleMenuModel { | 24 class TestModel : public ui::SimpleMenuModel { |
| 21 public: | 25 public: |
| 22 TestModel() : ui::SimpleMenuModel(&delegate_), delegate_(this) {} | 26 TestModel() : ui::SimpleMenuModel(&delegate_), delegate_(this) {} |
| 23 | 27 |
| 24 void set_checked_command(int command) { checked_command_ = command; } | 28 void set_checked_command(int command) { checked_command_ = command; } |
| 25 | 29 |
| 26 void set_menu_open_callback(const base::Closure& callback) { | 30 void set_menu_open_callback(const base::Closure& callback) { |
| 27 menu_open_callback_ = callback; | 31 menu_open_callback_ = callback; |
| 28 } | 32 } |
| 29 | 33 |
| 30 private: | 34 private: |
| 31 class Delegate : public ui::SimpleMenuModel::Delegate { | 35 class Delegate : public ui::SimpleMenuModel::Delegate { |
| 32 public: | 36 public: |
| 33 explicit Delegate(TestModel* model) : model_(model) {} | 37 explicit Delegate(TestModel* model) : model_(model) {} |
| 34 bool IsCommandIdChecked(int command_id) const override { | 38 bool IsCommandIdChecked(int command_id) const override { |
| 35 return command_id == model_->checked_command_; | 39 return command_id == model_->checked_command_; |
| 36 } | 40 } |
| 37 bool IsCommandIdEnabled(int command_id) const override { return true; } | 41 bool IsCommandIdEnabled(int command_id) const override { return true; } |
| 38 void ExecuteCommand(int command_id, int event_flags) override {} | 42 void ExecuteCommand(int command_id, int event_flags) override {} |
| 39 | 43 |
| 40 void MenuWillShow(SimpleMenuModel* source) override { | 44 void MenuWillShow(SimpleMenuModel* source) override { |
| 41 model_->menu_open_callback_.Run(); | 45 if (!model_->menu_open_callback_.is_null()) |
| 46 model_->menu_open_callback_.Run(); |
| 42 } | 47 } |
| 43 | 48 |
| 44 private: | 49 private: |
| 45 TestModel* model_; | 50 TestModel* model_; |
| 46 | 51 |
| 47 DISALLOW_COPY_AND_ASSIGN(Delegate); | 52 DISALLOW_COPY_AND_ASSIGN(Delegate); |
| 48 }; | 53 }; |
| 49 | 54 |
| 50 private: | 55 private: |
| 51 int checked_command_ = -1; | 56 int checked_command_ = -1; |
| 52 Delegate delegate_; | 57 Delegate delegate_; |
| 53 base::Closure menu_open_callback_; | 58 base::Closure menu_open_callback_; |
| 54 | 59 |
| 55 DISALLOW_COPY_AND_ASSIGN(TestModel); | 60 DISALLOW_COPY_AND_ASSIGN(TestModel); |
| 56 }; | 61 }; |
| 57 | 62 |
| 63 enum class MenuType { NATIVE, VIEWS }; |
| 64 |
| 65 std::string MenuTypeToString(::testing::TestParamInfo<MenuType> info) { |
| 66 return info.param == MenuType::VIEWS ? "VIEWS_MenuItemView" : "NATIVE_NSMenu"; |
| 67 } |
| 68 |
| 58 } // namespace | 69 } // namespace |
| 59 | 70 |
| 60 class MenuRunnerCocoaTest : public ViewsTestBase { | 71 class MenuRunnerCocoaTest : public ViewsTestBase, |
| 72 public ::testing::WithParamInterface<MenuType> { |
| 61 public: | 73 public: |
| 62 enum { | 74 enum { |
| 63 kWindowHeight = 200, | 75 kWindowHeight = 200, |
| 64 kWindowOffset = 100, | 76 kWindowOffset = 100, |
| 65 }; | 77 }; |
| 66 | 78 |
| 67 MenuRunnerCocoaTest() {} | 79 MenuRunnerCocoaTest() {} |
| 68 ~MenuRunnerCocoaTest() override {} | 80 ~MenuRunnerCocoaTest() override {} |
| 69 | 81 |
| 70 void SetUp() override { | 82 void SetUp() override { |
| 71 const int kWindowWidth = 300; | 83 const int kWindowWidth = 300; |
| 72 ViewsTestBase::SetUp(); | 84 ViewsTestBase::SetUp(); |
| 73 | 85 |
| 74 menu_.reset(new TestModel()); | 86 menu_.reset(new TestModel()); |
| 75 menu_->AddCheckItem(0, base::ASCIIToUTF16("Menu Item")); | 87 menu_->AddCheckItem(0, base::ASCIIToUTF16("Menu Item")); |
| 76 | 88 |
| 77 parent_ = new views::Widget(); | 89 parent_ = new views::Widget(); |
| 78 parent_->Init(CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS)); | 90 parent_->Init(CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS)); |
| 79 parent_->SetBounds( | 91 parent_->SetBounds( |
| 80 gfx::Rect(kWindowOffset, kWindowOffset, kWindowWidth, kWindowHeight)); | 92 gfx::Rect(kWindowOffset, kWindowOffset, kWindowWidth, kWindowHeight)); |
| 81 parent_->Show(); | 93 parent_->Show(); |
| 82 | 94 |
| 83 runner_ = new internal::MenuRunnerImplCocoa(menu_.get()); | 95 base::Closure on_close = base::Bind(&MenuRunnerCocoaTest::MenuCloseCallback, |
| 96 base::Unretained(this)); |
| 97 if (GetParam() == MenuType::NATIVE) |
| 98 runner_ = new internal::MenuRunnerImplCocoa(menu_.get(), on_close); |
| 99 else |
| 100 runner_ = new internal::MenuRunnerImplAdapter(menu_.get(), on_close); |
| 84 EXPECT_FALSE(runner_->IsRunning()); | 101 EXPECT_FALSE(runner_->IsRunning()); |
| 85 } | 102 } |
| 86 | 103 |
| 87 void TearDown() override { | 104 void TearDown() override { |
| 88 if (runner_) { | 105 if (runner_) { |
| 89 runner_->Release(); | 106 runner_->Release(); |
| 90 runner_ = NULL; | 107 runner_ = NULL; |
| 91 } | 108 } |
| 92 | 109 |
| 93 parent_->CloseNow(); | 110 parent_->CloseNow(); |
| 94 ViewsTestBase::TearDown(); | 111 ViewsTestBase::TearDown(); |
| 95 } | 112 } |
| 96 | 113 |
| 114 int IsAsync() const { return GetParam() == MenuType::VIEWS; } |
| 115 |
| 97 // Runs the menu after registering |callback| as the menu open callback. | 116 // Runs the menu after registering |callback| as the menu open callback. |
| 98 MenuRunner::RunResult RunMenu(const base::Closure& callback) { | 117 MenuRunner::RunResult RunMenu(const base::Closure& callback) { |
| 99 menu_->set_menu_open_callback( | 118 if (IsAsync()) { |
| 100 base::Bind(&MenuRunnerCocoaTest::RunMenuWrapperCallback, | 119 // Cancelling an async menu under MenuController::OpenMenuImpl() (which |
| 101 base::Unretained(this), callback)); | 120 // invokes WillShowMenu()) will cause a UAF when that same function tries |
| 102 return runner_->RunMenuAt(parent_, nullptr, gfx::Rect(), | 121 // to show the menu. So post a task instead. |
| 103 MENU_ANCHOR_TOPLEFT, MenuRunner::CONTEXT_MENU); | 122 base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, callback); |
| 123 } else { |
| 124 menu_->set_menu_open_callback( |
| 125 base::Bind(&MenuRunnerCocoaTest::RunMenuWrapperCallback, |
| 126 base::Unretained(this), callback)); |
| 127 } |
| 128 |
| 129 // Always pass ASYNC, even though native menus will be sync. |
| 130 int run_types = MenuRunner::CONTEXT_MENU | MenuRunner::ASYNC; |
| 131 return MaybeRunAsync(runner_->RunMenuAt(parent_, nullptr, gfx::Rect(), |
| 132 MENU_ANCHOR_TOPLEFT, run_types)); |
| 104 } | 133 } |
| 105 | 134 |
| 106 // Runs then cancels a combobox menu and captures the frame of the anchoring | 135 // Runs then cancels a combobox menu and captures the frame of the anchoring |
| 107 // view. | 136 // view. |
| 108 MenuRunner::RunResult RunMenuAt(const gfx::Rect& anchor) { | 137 MenuRunner::RunResult RunMenuAt(const gfx::Rect& anchor) { |
| 109 last_anchor_frame_ = NSZeroRect; | 138 last_anchor_frame_ = NSZeroRect; |
| 110 | 139 |
| 111 // Should be one child (the compositor layer) before showing, and it should | 140 // Should be one child (the compositor layer) before showing, and it should |
| 112 // go up by one (the anchor view) while the menu is shown. | 141 // go up by one (the anchor view) while the menu is shown. |
| 113 EXPECT_EQ(1u, [[parent_->GetNativeView() subviews] count]); | 142 EXPECT_EQ(1u, [[parent_->GetNativeView() subviews] count]); |
| 114 | 143 |
| 115 menu_->set_menu_open_callback(base::Bind( | 144 base::Closure callback = |
| 116 &MenuRunnerCocoaTest::RunMenuAtCallback, base::Unretained(this))); | 145 base::Bind(&MenuRunnerCocoaTest::ComboboxRunMenuAtCallback, |
| 146 base::Unretained(this)); |
| 147 if (IsAsync()) |
| 148 base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, callback); |
| 149 else |
| 150 menu_->set_menu_open_callback(callback); |
| 117 | 151 |
| 118 MenuRunner::RunResult result = runner_->RunMenuAt( | 152 MenuRunner::RunResult result = MaybeRunAsync( |
| 119 parent_, nullptr, anchor, MENU_ANCHOR_TOPLEFT, MenuRunner::COMBOBOX); | 153 runner_->RunMenuAt(parent_, nullptr, anchor, MENU_ANCHOR_TOPLEFT, |
| 154 MenuRunner::COMBOBOX | MenuRunner::ASYNC)); |
| 120 | 155 |
| 121 // Ensure the anchor view is removed. | 156 // Ensure the anchor view is removed. |
| 122 EXPECT_EQ(1u, [[parent_->GetNativeView() subviews] count]); | 157 EXPECT_EQ(1u, [[parent_->GetNativeView() subviews] count]); |
| 123 return result; | 158 return result; |
| 124 } | 159 } |
| 125 | 160 |
| 126 void MenuCancelCallback() { | 161 void MenuCancelCallback() { |
| 127 runner_->Cancel(); | 162 runner_->Cancel(); |
| 128 // For a syncronous menu, MenuRunner::IsRunning() should return true | 163 if (IsAsync()) { |
| 129 // immediately after MenuRunner::Cancel() since the menu message loop has | 164 // Async menus report their cancellation immediately. |
| 130 // not yet terminated. It has only been marked for termination. | 165 EXPECT_FALSE(runner_->IsRunning()); |
| 131 EXPECT_TRUE(runner_->IsRunning()); | 166 } else { |
| 167 // For a synchronous menu, MenuRunner::IsRunning() should return true |
| 168 // immediately after MenuRunner::Cancel() since the menu message loop has |
| 169 // not yet terminated. It has only been marked for termination. |
| 170 EXPECT_TRUE(runner_->IsRunning()); |
| 171 } |
| 132 } | 172 } |
| 133 | 173 |
| 134 void MenuDeleteCallback() { | 174 void MenuDeleteCallback() { |
| 135 runner_->Release(); | 175 runner_->Release(); |
| 136 runner_ = nullptr; | 176 runner_ = nullptr; |
| 177 // Deleting an async menu intentionally does not invoke MenuCloseCallback(). |
| 178 // (The callback is typically a method on something in the process of being |
| 179 // destroyed). So invoke QuitAsyncRunLoop() here as well. |
| 180 QuitAsyncRunLoop(); |
| 137 } | 181 } |
| 138 | 182 |
| 139 void MenuCancelAndDeleteCallback() { | 183 void MenuCancelAndDeleteCallback() { |
| 140 runner_->Cancel(); | 184 runner_->Cancel(); |
| 141 runner_->Release(); | 185 runner_->Release(); |
| 142 runner_ = nullptr; | 186 runner_ = nullptr; |
| 143 } | 187 } |
| 144 | 188 |
| 145 protected: | 189 protected: |
| 146 std::unique_ptr<TestModel> menu_; | 190 std::unique_ptr<TestModel> menu_; |
| 147 internal::MenuRunnerImplCocoa* runner_ = nullptr; | 191 internal::MenuRunnerImplInterface* runner_ = nullptr; |
| 148 views::Widget* parent_ = nullptr; | 192 views::Widget* parent_ = nullptr; |
| 149 NSRect last_anchor_frame_ = NSZeroRect; | 193 NSRect last_anchor_frame_ = NSZeroRect; |
| 194 int menu_close_count_ = 0; |
| 150 | 195 |
| 151 private: | 196 private: |
| 152 void RunMenuWrapperCallback(const base::Closure& callback) { | 197 void RunMenuWrapperCallback(const base::Closure& callback) { |
| 153 EXPECT_TRUE(runner_->IsRunning()); | 198 EXPECT_TRUE(runner_->IsRunning()); |
| 154 callback.Run(); | 199 callback.Run(); |
| 155 } | 200 } |
| 156 | 201 |
| 157 void RunMenuAtCallback() { | 202 void ComboboxRunMenuAtCallback() { |
| 158 NSArray* subviews = [parent_->GetNativeView() subviews]; | 203 NSArray* subviews = [parent_->GetNativeView() subviews]; |
| 159 EXPECT_EQ(2u, [subviews count]); | 204 // An anchor view should only be added for Native menus. |
| 160 last_anchor_frame_ = [[subviews objectAtIndex:1] frame]; | 205 if (GetParam() == MenuType::NATIVE) { |
| 206 ASSERT_EQ(2u, [subviews count]); |
| 207 last_anchor_frame_ = [[subviews objectAtIndex:1] frame]; |
| 208 } else { |
| 209 EXPECT_EQ(1u, [subviews count]); |
| 210 } |
| 161 runner_->Cancel(); | 211 runner_->Cancel(); |
| 162 } | 212 } |
| 163 | 213 |
| 214 // Run a nested message loop so that async and sync menus can be tested the |
| 215 // same way. |
| 216 MenuRunner::RunResult MaybeRunAsync(MenuRunner::RunResult run_result) { |
| 217 if (!IsAsync()) |
| 218 return run_result; |
| 219 |
| 220 // Async menus should always return NORMAL_EXIT. |
| 221 EXPECT_EQ(MenuRunner::NORMAL_EXIT, run_result); |
| 222 base::RunLoop run_loop; |
| 223 quit_closure_ = run_loop.QuitClosure(); |
| 224 base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| 225 FROM_HERE, quit_closure_, TestTimeouts::action_timeout()); |
| 226 run_loop.Run(); |
| 227 |
| 228 // |quit_closure_| should be run by QuitAsyncRunLoop(), not the timeout. |
| 229 EXPECT_TRUE(quit_closure_.is_null()); |
| 230 return run_result; |
| 231 } |
| 232 |
| 233 void QuitAsyncRunLoop() { |
| 234 if (!IsAsync()) { |
| 235 EXPECT_TRUE(quit_closure_.is_null()); |
| 236 return; |
| 237 } |
| 238 ASSERT_FALSE(quit_closure_.is_null()); |
| 239 quit_closure_.Run(); |
| 240 quit_closure_.Reset(); |
| 241 } |
| 242 |
| 243 void MenuCloseCallback() { |
| 244 ++menu_close_count_; |
| 245 QuitAsyncRunLoop(); |
| 246 } |
| 247 |
| 248 base::Closure quit_closure_; |
| 249 |
| 164 DISALLOW_COPY_AND_ASSIGN(MenuRunnerCocoaTest); | 250 DISALLOW_COPY_AND_ASSIGN(MenuRunnerCocoaTest); |
| 165 }; | 251 }; |
| 166 | 252 |
| 167 TEST_F(MenuRunnerCocoaTest, RunMenuAndCancel) { | 253 TEST_P(MenuRunnerCocoaTest, RunMenuAndCancel) { |
| 168 base::TimeTicks min_time = ui::EventTimeForNow(); | 254 base::TimeTicks min_time = ui::EventTimeForNow(); |
| 169 | 255 |
| 170 MenuRunner::RunResult result = RunMenu(base::Bind( | 256 MenuRunner::RunResult result = RunMenu(base::Bind( |
| 171 &MenuRunnerCocoaTest::MenuCancelCallback, base::Unretained(this))); | 257 &MenuRunnerCocoaTest::MenuCancelCallback, base::Unretained(this))); |
| 172 | 258 |
| 259 EXPECT_EQ(1, menu_close_count_); |
| 173 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result); | 260 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result); |
| 174 EXPECT_FALSE(runner_->IsRunning()); | 261 EXPECT_FALSE(runner_->IsRunning()); |
| 175 | 262 |
| 176 EXPECT_GE(runner_->GetClosingEventTime(), min_time); | 263 if (GetParam() == MenuType::VIEWS) { |
| 264 // MenuItemView's MenuRunnerImpl gets the closing time from MenuController:: |
| 265 // closing_event_time(). This is is reset on show, but only updated when an |
| 266 // event closes the menu -- not a cancellation. |
| 267 EXPECT_EQ(runner_->GetClosingEventTime(), base::TimeTicks()); |
| 268 } else { |
| 269 EXPECT_GE(runner_->GetClosingEventTime(), min_time); |
| 270 } |
| 177 EXPECT_LE(runner_->GetClosingEventTime(), ui::EventTimeForNow()); | 271 EXPECT_LE(runner_->GetClosingEventTime(), ui::EventTimeForNow()); |
| 178 | 272 |
| 179 // Cancel again. | 273 // Cancel again. |
| 180 runner_->Cancel(); | 274 runner_->Cancel(); |
| 181 EXPECT_FALSE(runner_->IsRunning()); | 275 EXPECT_FALSE(runner_->IsRunning()); |
| 276 EXPECT_EQ(1, menu_close_count_); |
| 182 } | 277 } |
| 183 | 278 |
| 184 TEST_F(MenuRunnerCocoaTest, RunMenuAndDelete) { | 279 TEST_P(MenuRunnerCocoaTest, RunMenuAndDelete) { |
| 185 MenuRunner::RunResult result = RunMenu(base::Bind( | 280 MenuRunner::RunResult result = RunMenu(base::Bind( |
| 186 &MenuRunnerCocoaTest::MenuDeleteCallback, base::Unretained(this))); | 281 &MenuRunnerCocoaTest::MenuDeleteCallback, base::Unretained(this))); |
| 187 EXPECT_EQ(MenuRunner::MENU_DELETED, result); | 282 // Note the close callback is NOT invoked for deleted menus. |
| 283 EXPECT_EQ(0, menu_close_count_); |
| 284 |
| 285 // Async menus always return NORMAL from RunMenuAt(). |
| 286 if (GetParam() == MenuType::VIEWS) |
| 287 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result); |
| 288 else |
| 289 EXPECT_EQ(MenuRunner::MENU_DELETED, result); |
| 188 } | 290 } |
| 189 | 291 |
| 190 // Ensure a menu can be safely released immediately after a call to Cancel() in | 292 // Ensure a menu can be safely released immediately after a call to Cancel() in |
| 191 // the same run loop iteration. | 293 // the same run loop iteration. |
| 192 TEST_F(MenuRunnerCocoaTest, DestroyAfterCanceling) { | 294 TEST_P(MenuRunnerCocoaTest, DestroyAfterCanceling) { |
| 193 MenuRunner::RunResult result = | 295 MenuRunner::RunResult result = |
| 194 RunMenu(base::Bind(&MenuRunnerCocoaTest::MenuCancelAndDeleteCallback, | 296 RunMenu(base::Bind(&MenuRunnerCocoaTest::MenuCancelAndDeleteCallback, |
| 195 base::Unretained(this))); | 297 base::Unretained(this))); |
| 196 EXPECT_EQ(MenuRunner::MENU_DELETED, result); | 298 |
| 299 if (IsAsync()) { |
| 300 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result); |
| 301 EXPECT_EQ(1, menu_close_count_); |
| 302 } else { |
| 303 EXPECT_EQ(MenuRunner::MENU_DELETED, result); |
| 304 // For a synchronous menu, the deletion happens before the cancel can be |
| 305 // processed, so the close callback will not be invoked. |
| 306 EXPECT_EQ(0, menu_close_count_); |
| 307 } |
| 197 } | 308 } |
| 198 | 309 |
| 199 TEST_F(MenuRunnerCocoaTest, RunMenuTwice) { | 310 TEST_P(MenuRunnerCocoaTest, RunMenuTwice) { |
| 200 for (int i = 0; i < 2; ++i) { | 311 for (int i = 0; i < 2; ++i) { |
| 201 MenuRunner::RunResult result = RunMenu(base::Bind( | 312 MenuRunner::RunResult result = RunMenu(base::Bind( |
| 202 &MenuRunnerCocoaTest::MenuCancelCallback, base::Unretained(this))); | 313 &MenuRunnerCocoaTest::MenuCancelCallback, base::Unretained(this))); |
| 203 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result); | 314 EXPECT_EQ(MenuRunner::NORMAL_EXIT, result); |
| 204 EXPECT_FALSE(runner_->IsRunning()); | 315 EXPECT_FALSE(runner_->IsRunning()); |
| 316 EXPECT_EQ(i + 1, menu_close_count_); |
| 205 } | 317 } |
| 206 } | 318 } |
| 207 | 319 |
| 208 TEST_F(MenuRunnerCocoaTest, CancelWithoutRunning) { | 320 TEST_P(MenuRunnerCocoaTest, CancelWithoutRunning) { |
| 209 runner_->Cancel(); | 321 runner_->Cancel(); |
| 210 EXPECT_FALSE(runner_->IsRunning()); | 322 EXPECT_FALSE(runner_->IsRunning()); |
| 211 EXPECT_EQ(base::TimeTicks(), runner_->GetClosingEventTime()); | 323 EXPECT_EQ(base::TimeTicks(), runner_->GetClosingEventTime()); |
| 324 EXPECT_EQ(0, menu_close_count_); |
| 212 } | 325 } |
| 213 | 326 |
| 214 TEST_F(MenuRunnerCocoaTest, DeleteWithoutRunning) { | 327 TEST_P(MenuRunnerCocoaTest, DeleteWithoutRunning) { |
| 215 runner_->Release(); | 328 runner_->Release(); |
| 216 runner_ = NULL; | 329 runner_ = NULL; |
| 330 EXPECT_EQ(0, menu_close_count_); |
| 217 } | 331 } |
| 218 | 332 |
| 219 // Tests anchoring of the menus used for toolkit-views Comboboxes. | 333 // Tests anchoring of the menus used for toolkit-views Comboboxes. |
| 220 TEST_F(MenuRunnerCocoaTest, ComboboxAnchoring) { | 334 TEST_P(MenuRunnerCocoaTest, ComboboxAnchoring) { |
| 221 // Combobox at 20,10 in the Widget. | 335 // Combobox at 20,10 in the Widget. |
| 222 const gfx::Rect combobox_rect(20, 10, 80, 50); | 336 const gfx::Rect combobox_rect(20, 10, 80, 50); |
| 223 | 337 |
| 224 // Menu anchor rects are always in screen coordinates. The window is frameless | 338 // Menu anchor rects are always in screen coordinates. The window is frameless |
| 225 // so offset by the bounds. | 339 // so offset by the bounds. |
| 226 gfx::Rect anchor_rect = combobox_rect; | 340 gfx::Rect anchor_rect = combobox_rect; |
| 227 anchor_rect.Offset(kWindowOffset, kWindowOffset); | 341 anchor_rect.Offset(kWindowOffset, kWindowOffset); |
| 228 RunMenuAt(anchor_rect); | 342 RunMenuAt(anchor_rect); |
| 229 | 343 |
| 344 if (GetParam() != MenuType::NATIVE) { |
| 345 // Combobox anchoring is only implemented for native menus. |
| 346 EXPECT_NSEQ(NSZeroRect, last_anchor_frame_); |
| 347 return; |
| 348 } |
| 349 |
| 230 // Nothing is checked, so the anchor view should have no height, to ensure the | 350 // Nothing is checked, so the anchor view should have no height, to ensure the |
| 231 // menu goes below the anchor rect. There should also be no x-offset since the | 351 // menu goes below the anchor rect. There should also be no x-offset since the |
| 232 // there is no need to line-up text. | 352 // there is no need to line-up text. |
| 233 EXPECT_NSEQ( | 353 EXPECT_NSEQ( |
| 234 NSMakeRect(combobox_rect.x(), kWindowHeight - combobox_rect.bottom(), | 354 NSMakeRect(combobox_rect.x(), kWindowHeight - combobox_rect.bottom(), |
| 235 combobox_rect.width(), 0), | 355 combobox_rect.width(), 0), |
| 236 last_anchor_frame_); | 356 last_anchor_frame_); |
| 237 | 357 |
| 238 menu_->set_checked_command(0); | 358 menu_->set_checked_command(0); |
| 239 RunMenuAt(anchor_rect); | 359 RunMenuAt(anchor_rect); |
| (...skipping 11 matching lines...) Expand all Loading... |
| 251 EXPECT_NE(0, NSHeight(last_anchor_frame_)); | 371 EXPECT_NE(0, NSHeight(last_anchor_frame_)); |
| 252 | 372 |
| 253 // In RTL, Cocoa messes up the positioning unless the anchor rectangle is | 373 // In RTL, Cocoa messes up the positioning unless the anchor rectangle is |
| 254 // offset to the right of the view. The offset for the checkmark is also | 374 // offset to the right of the view. The offset for the checkmark is also |
| 255 // skipped, to give a better match to native behavior. | 375 // skipped, to give a better match to native behavior. |
| 256 base::i18n::SetICUDefaultLocale("he"); | 376 base::i18n::SetICUDefaultLocale("he"); |
| 257 RunMenuAt(anchor_rect); | 377 RunMenuAt(anchor_rect); |
| 258 EXPECT_EQ(combobox_rect.right(), last_anchor_frame_.origin.x); | 378 EXPECT_EQ(combobox_rect.right(), last_anchor_frame_.origin.x); |
| 259 } | 379 } |
| 260 | 380 |
| 381 INSTANTIATE_TEST_CASE_P(, |
| 382 MenuRunnerCocoaTest, |
| 383 ::testing::Values(MenuType::NATIVE, MenuType::VIEWS), |
| 384 &MenuTypeToString); |
| 385 |
| 261 } // namespace test | 386 } // namespace test |
| 262 } // namespace views | 387 } // namespace views |
| OLD | NEW |