Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 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 <Cocoa/Cocoa.h> | 5 #import <Cocoa/Cocoa.h> |
| 6 | 6 |
| 7 #include "base/mac/mac_util.h" | |
| 7 #include "base/message_loop/message_loop.h" | 8 #include "base/message_loop/message_loop.h" |
| 8 #include "base/run_loop.h" | 9 #include "base/run_loop.h" |
| 9 #include "base/strings/sys_string_conversions.h" | 10 #include "base/strings/sys_string_conversions.h" |
| 10 #include "base/strings/utf_string_conversions.h" | 11 #include "base/strings/utf_string_conversions.h" |
| 11 #include "third_party/skia/include/core/SkBitmap.h" | 12 #include "third_party/skia/include/core/SkBitmap.h" |
| 12 #import "ui/base/cocoa/menu_controller.h" | 13 #import "ui/base/cocoa/menu_controller.h" |
| 13 #include "ui/base/models/simple_menu_model.h" | 14 #include "ui/base/models/simple_menu_model.h" |
| 14 #include "ui/base/resource/resource_bundle.h" | 15 #include "ui/base/resource/resource_bundle.h" |
| 15 #include "ui/events/test/cocoa_test_event_utils.h" | 16 #include "ui/events/test/cocoa_test_event_utils.h" |
| 16 #include "ui/gfx/image/image.h" | 17 #include "ui/gfx/image/image.h" |
| 17 #import "ui/gfx/test/ui_cocoa_test_helper.h" | 18 #import "ui/gfx/test/ui_cocoa_test_helper.h" |
| 18 #include "ui/resources/grit/ui_resources.h" | 19 #include "ui/resources/grit/ui_resources.h" |
| 19 #include "ui/strings/grit/ui_strings.h" | 20 #include "ui/strings/grit/ui_strings.h" |
| 20 | 21 |
| 21 using base::ASCIIToUTF16; | 22 using base::ASCIIToUTF16; |
| 22 | 23 |
| 24 @interface MenuController (TestingAPI) | |
| 25 - (void)itemWillBeSelected:(NSMenuItem*)sender; | |
| 26 - (void)itemSelected:(id)sender; | |
| 27 @end | |
| 28 | |
| 29 @interface TestResponsiveMenuController : MenuController | |
| 30 @property(assign, nonatomic) BOOL sawItemEarly; | |
| 31 @end | |
| 32 | |
| 33 @implementation TestResponsiveMenuController { | |
| 34 BOOL sawItemEarly_; | |
| 35 } | |
| 36 | |
| 37 @synthesize sawItemEarly = sawItemEarly_; | |
| 38 | |
| 39 - (void)itemWillBeSelected:(NSMenuItem*)sender { | |
| 40 sawItemEarly_ = YES; | |
| 41 [super itemWillBeSelected:sender]; | |
| 42 } | |
| 43 | |
| 44 @end | |
| 45 | |
| 46 @interface NSMenuItem (Private) | |
| 47 // Exposed to simulate in testing. | |
| 48 - (void)_sendItemSelectedNote; | |
| 49 @end | |
| 50 | |
| 23 namespace ui { | 51 namespace ui { |
| 24 | 52 |
| 25 namespace { | 53 namespace { |
| 26 | 54 |
| 27 const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE; | 55 const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE; |
| 28 | 56 |
| 29 class MenuControllerTest : public CocoaTest { | 57 class MenuControllerTest : public CocoaTest { |
| 30 }; | 58 }; |
| 31 | 59 |
| 32 // A menu delegate that counts the number of times certain things are called | 60 // A menu delegate that counts the number of times certain things are called |
| 33 // to make sure things are hooked up properly. | 61 // to make sure things are hooked up properly. |
| 34 class Delegate : public SimpleMenuModel::Delegate { | 62 class Delegate : public SimpleMenuModel::Delegate { |
| 35 public: | 63 public: |
| 36 Delegate() | 64 Delegate() {} |
| 37 : execute_count_(0), | |
| 38 enable_count_(0), | |
| 39 menu_to_close_(nil), | |
| 40 did_show_(false), | |
| 41 did_close_(false) { | |
| 42 } | |
| 43 | 65 |
| 44 bool IsCommandIdChecked(int command_id) const override { return false; } | 66 bool IsCommandIdChecked(int command_id) const override { return false; } |
| 45 bool IsCommandIdEnabled(int command_id) const override { | 67 bool IsCommandIdEnabled(int command_id) const override { |
| 46 ++enable_count_; | 68 ++enable_count_; |
| 47 return true; | 69 return true; |
| 48 } | 70 } |
| 49 void ExecuteCommand(int command_id, int event_flags) override { | 71 void ExecuteCommand(int command_id, int event_flags) override { |
| 50 ++execute_count_; | 72 ++execute_count_; |
| 51 } | 73 } |
| 52 | 74 |
| 53 void MenuWillShow(SimpleMenuModel* /*source*/) override { | 75 void MenuWillShow(SimpleMenuModel* /*source*/) override { |
| 54 EXPECT_FALSE(did_show_); | 76 EXPECT_FALSE(did_show_); |
| 55 EXPECT_FALSE(did_close_); | 77 EXPECT_FALSE(did_close_); |
| 56 did_show_ = true; | 78 did_show_ = true; |
| 57 NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode, | 79 if (auto_close_) { |
| 58 NSDefaultRunLoopMode, | 80 NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode, |
| 59 nil]; | 81 NSDefaultRunLoopMode, nil]; |
| 60 [menu_to_close_ performSelector:@selector(cancelTracking) | 82 [menu_to_close_ performSelector:@selector(cancelTracking) |
| 61 withObject:nil | 83 withObject:nil |
| 62 afterDelay:0.1 | 84 afterDelay:0.1 |
| 63 inModes:modes]; | 85 inModes:modes]; |
| 86 } | |
| 64 } | 87 } |
| 65 | 88 |
| 66 void MenuClosed(SimpleMenuModel* /*source*/) override { | 89 void MenuClosed(SimpleMenuModel* /*source*/) override { |
| 67 EXPECT_TRUE(did_show_); | 90 EXPECT_TRUE(did_show_); |
| 68 EXPECT_FALSE(did_close_); | 91 EXPECT_FALSE(did_close_); |
| 92 DCHECK(!did_close_); | |
| 69 did_close_ = true; | 93 did_close_ = true; |
| 70 } | 94 } |
| 71 | 95 |
| 72 int execute_count_; | 96 int execute_count_ = 0; |
| 73 mutable int enable_count_; | 97 mutable int enable_count_ = 0; |
| 74 // The menu on which to call |-cancelTracking| after a short delay in | 98 // The menu on which to call |-cancelTracking| after a short delay in |
| 75 // MenuWillShow. | 99 // MenuWillShow. |
| 76 NSMenu* menu_to_close_; | 100 NSMenu* menu_to_close_ = nil; |
| 77 bool did_show_; | 101 bool did_show_ = false; |
| 78 bool did_close_; | 102 bool did_close_ = false; |
| 103 bool auto_close_ = true; | |
| 104 | |
| 105 private: | |
| 106 DISALLOW_COPY_AND_ASSIGN(Delegate); | |
| 79 }; | 107 }; |
| 80 | 108 |
| 81 // Just like Delegate, except the items are treated as "dynamic" so updates to | 109 // Just like Delegate, except the items are treated as "dynamic" so updates to |
| 82 // the label/icon in the model are reflected in the menu. | 110 // the label/icon in the model are reflected in the menu. |
| 83 class DynamicDelegate : public Delegate { | 111 class DynamicDelegate : public Delegate { |
| 84 public: | 112 public: |
| 85 DynamicDelegate() {} | 113 DynamicDelegate() {} |
| 86 bool IsItemForCommandIdDynamic(int command_id) const override { return true; } | 114 bool IsItemForCommandIdDynamic(int command_id) const override { return true; } |
| 87 base::string16 GetLabelForCommandId(int command_id) const override { | 115 base::string16 GetLabelForCommandId(int command_id) const override { |
| 88 return label_; | 116 return label_; |
| (...skipping 297 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 386 // |did_close_| will remain false until we pump the task manually. | 414 // |did_close_| will remain false until we pump the task manually. |
| 387 EXPECT_FALSE(delegate.did_close_); | 415 EXPECT_FALSE(delegate.did_close_); |
| 388 | 416 |
| 389 // Pump the task that notifies the delegate. | 417 // Pump the task that notifies the delegate. |
| 390 base::RunLoop().RunUntilIdle(); | 418 base::RunLoop().RunUntilIdle(); |
| 391 | 419 |
| 392 // Expect that the delegate got notified properly. | 420 // Expect that the delegate got notified properly. |
| 393 EXPECT_TRUE(delegate.did_close_); | 421 EXPECT_TRUE(delegate.did_close_); |
| 394 } | 422 } |
| 395 | 423 |
| 424 // Verify that the private API used by MenuController's ResponsiveNSMenuItem | |
| 425 // exists in the runtime. It's not a disaster if it disappears, (or AppKit | |
| 426 // stops invoking it) but consumers will stop receiving opportunities to | |
| 427 // -processItemSelectedEarly:. | |
| 428 TEST_F(MenuControllerTest, SendItemSelectedNoteExists) { | |
| 429 // -_sendItemSelectedNote doesn't exist on 10.9 or 10.10. NSPopUpButton menus | |
| 430 // on 10.9 don't animate out, and always suffer from the brief "flash" of the | |
| 431 // old selection when the menu disappears. | |
| 432 // TODO(tapted): Find a hook on 10.10 if we deem it necessary. | |
| 433 if (base::mac::IsAtMostOS10_10()) | |
| 434 return; | |
| 435 | |
| 436 EXPECT_TRUE( | |
| 437 [NSMenuItem instancesRespondToSelector:@selector(_sendItemSelectedNote)]); | |
| 438 } | |
| 439 | |
| 440 // Emulate the flow for -[MenuController itemWillBeSelected:] and processing the | |
| 441 // action via posted task during menu fade out. | |
| 442 TEST_F(MenuControllerTest, EmulateItemSelectedEarly) { | |
| 443 if (![NSMenuItem instancesRespondToSelector:@selector(_sendItemSelectedNote)]) | |
| 444 return; | |
| 445 | |
| 446 base::MessageLoopForUI message_loop; | |
| 447 | |
| 448 Delegate delegate; | |
| 449 delegate.auto_close_ = false; | |
| 450 | |
| 451 SimpleMenuModel model(&delegate); | |
| 452 model.AddItem(1, ASCIIToUTF16("foo")); | |
| 453 | |
| 454 base::scoped_nsobject<TestResponsiveMenuController> controller( | |
| 455 [[TestResponsiveMenuController alloc] initWithModel:&model | |
| 456 useWithPopUpButtonCell:NO]); | |
| 457 | |
| 458 auto ResetWithPostTask = [&](BOOL post) { | |
| 459 // Flush calls to OnMenuClosed() Posted by SimpleMenuModel. | |
| 460 base::RunLoop().RunUntilIdle(); | |
| 461 | |
| 462 [controller setPostItemSelectedAsTask:post]; | |
| 463 [controller setSawItemEarly:NO]; | |
| 464 delegate.execute_count_ = 0; | |
| 465 delegate.did_show_ = delegate.did_close_ = false; | |
| 466 }; | |
| 467 | |
| 468 ResetWithPostTask(YES); | |
| 469 NSMenuItem* item = [[controller menu] itemAtIndex:0]; | |
| 470 EXPECT_TRUE(item); | |
| 471 | |
| 472 [controller menuWillOpen:[controller menu]]; | |
| 473 | |
| 474 // Pretend the first item got clicked. AppKit sends _sendItemSelectedNote to | |
| 475 // the menu item, then performs its action. | |
| 476 EXPECT_FALSE([controller sawItemEarly]); | |
| 477 EXPECT_EQ(0, delegate.execute_count_); | |
| 478 [item _sendItemSelectedNote]; | |
| 479 | |
| 480 EXPECT_TRUE([controller sawItemEarly]); | |
| 481 | |
| 482 // Task is posted at this point, but not executed. | |
| 483 EXPECT_EQ(0, delegate.execute_count_); | |
| 484 | |
| 485 // Pretend the menu is fading out, which spins a RunLoop. | |
| 486 base::RunLoop().RunUntilIdle(); | |
| 487 | |
| 488 // Item gets executed early. | |
| 489 EXPECT_EQ(1, delegate.execute_count_); | |
| 490 | |
| 491 // Simulate dismissal. This happens before the action. | |
| 492 [controller menuDidClose:[controller menu]]; | |
| 493 | |
| 494 // Perform the action normally. Shouldn't get executed again. | |
| 495 [[item target] performSelector:[item action] withObject:item]; | |
| 496 EXPECT_EQ(1, delegate.execute_count_); | |
| 497 | |
| 498 // Repeat, simulating the condition where the private API hook fails. | |
| 499 ResetWithPostTask(YES); | |
| 500 [controller menuWillOpen:[controller menu]]; | |
| 501 [controller menuDidClose:[controller menu]]; | |
| 502 base::RunLoop().RunUntilIdle(); | |
| 503 EXPECT_FALSE([controller sawItemEarly]); | |
| 504 EXPECT_EQ(0, delegate.execute_count_); | |
| 505 [[item target] performSelector:[item action] withObject:item]; | |
| 506 EXPECT_FALSE([controller sawItemEarly]); | |
| 507 EXPECT_EQ(1, delegate.execute_count_); | |
| 508 | |
| 509 // Repeat, simulating the condition where events do not pump during fade out. | |
| 510 ResetWithPostTask(YES); | |
| 511 [controller menuWillOpen:[controller menu]]; | |
| 512 EXPECT_FALSE([controller sawItemEarly]); | |
| 513 EXPECT_EQ(0, delegate.execute_count_); | |
| 514 [item _sendItemSelectedNote]; | |
| 515 EXPECT_TRUE([controller sawItemEarly]); | |
| 516 EXPECT_EQ(0, delegate.execute_count_); | |
| 517 /* No pump. */ | |
|
Robert Sesek
2017/05/09 15:49:45
nit: /* -> //
tapted
2017/05/10 03:49:06
Done.
| |
| 518 [controller menuDidClose:[controller menu]]; | |
| 519 EXPECT_EQ(0, delegate.execute_count_); | |
| 520 [[item target] performSelector:[item action] withObject:item]; | |
| 521 EXPECT_TRUE([controller sawItemEarly]); | |
| 522 EXPECT_EQ(1, delegate.execute_count_); | |
| 523 base::RunLoop().RunUntilIdle(); // Back the main loop. | |
| 524 EXPECT_EQ(1, delegate.execute_count_); | |
| 525 | |
| 526 // Repeat, without processing early. | |
| 527 ResetWithPostTask(NO); | |
| 528 | |
| 529 [controller menuWillOpen:[controller menu]]; | |
| 530 [item _sendItemSelectedNote]; | |
| 531 | |
| 532 // Saw it, but didn't execute. | |
| 533 EXPECT_TRUE([controller sawItemEarly]); | |
| 534 EXPECT_EQ(0, delegate.execute_count_); | |
| 535 | |
| 536 // Even after spinning a RunLoop. | |
| 537 base::RunLoop().RunUntilIdle(); | |
| 538 EXPECT_EQ(0, delegate.execute_count_); | |
| 539 | |
| 540 [controller menuDidClose:[controller menu]]; | |
| 541 | |
| 542 // Perform the action normally. Now executes. | |
| 543 [[item target] performSelector:[item action] withObject:item]; | |
| 544 EXPECT_EQ(1, delegate.execute_count_); | |
| 545 } | |
| 546 | |
| 396 } // namespace | 547 } // namespace |
| 397 | 548 |
| 398 } // namespace ui | 549 } // namespace ui |
| OLD | NEW |