Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(17)

Side by Side Diff: ui/base/cocoa/menu_controller_unittest.mm

Issue 2852233002: Mac[Views]: Make native menus more responsive by pumping private runloop modes. (Closed)
Patch Set: fix tests, respond to comments, selfnits Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698