// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/base/cocoa/nsmenu_additions.h"
#include "base/test/gtest_util.h"
#include "testing/gtest/include/gtest/gtest.h"
@interface NSMenuAdditionsUnitTestMenuItem : NSMenuItem
@end
@implementation NSMenuAdditionsUnitTestMenuItem {
BOOL includeFunctionModifierInFlags_;
}
- (void)setKeyEquivalentModifierMask:(NSEventModifierFlags)mask {
// The AppKit ignores NSEventModifierFlagFunction when it's included
// in the mask. Note that it was included so we can fake it later.
includeFunctionModifierInFlags_ = (mask & NSEventModifierFlagFunction) > 0;
// Remove the flag to avoid a warning from the AppKit.
mask &= ~NSEventModifierFlagFunction;
[super setKeyEquivalentModifierMask:mask];
}
- (NSEventModifierFlags)keyEquivalentModifierMask {
NSEventModifierFlags flags = [super keyEquivalentModifierMask];
if (includeFunctionModifierInFlags_) {
flags |= NSEventModifierFlagFunction;
}
return flags;
}
@end
namespace {
NSMenu* Menu(NSString* title) {
NSMenu* menu = [[NSMenu alloc] initWithTitle:title];
menu.autoenablesItems = NO;
return menu;
}
NSMenuItem* MenuItem(NSString* title,
NSString* key_equivalent = @"",
NSEventModifierFlags modifier_mask = 0) {
NSMenuAdditionsUnitTestMenuItem* item =
[[NSMenuAdditionsUnitTestMenuItem alloc] initWithTitle:title
action:nil
keyEquivalent:key_equivalent];
item.keyEquivalentModifierMask = modifier_mask;
item.enabled = YES;
return item;
}
NSEvent* KeyEvent(const NSEventModifierFlags modifierFlags,
NSString* chars,
NSString* charsNoMods = nil,
const NSUInteger keyCode = 0) {
if (charsNoMods == nil)
charsNoMods = chars;
return [NSEvent keyEventWithType:NSEventTypeKeyDown
location:NSZeroPoint
modifierFlags:modifierFlags
timestamp:0.0
windowNumber:0
context:nil
characters:chars
charactersIgnoringModifiers:charsNoMods
isARepeat:NO
keyCode:keyCode];
}
} // namespace
TEST(NSMenuAdditionsTest, TestMenuItemForKeyEquivalentEvent) {
NSMenu* main_menu = Menu(@"Main Menu");
[main_menu addItem:MenuItem(@"App")];
NSString* file_title = @"File";
[main_menu addItem:MenuItem(file_title)];
[main_menu addItem:MenuItem(@"Edit")];
[main_menu addItem:MenuItem(@"Window")];
[main_menu addItem:MenuItem(@"Help")];
NSMenu* file_menu = Menu(file_title);
NSMenuItem* file_menu_item = [main_menu itemWithTitle:[file_menu title]];
[file_menu_item setSubmenu:file_menu];
[file_menu addItem:MenuItem(@"New")];
NSString* open_title = @"Open";
[file_menu addItem:MenuItem(open_title, @"o", NSEventModifierFlagCommand)];
NSString* open_recent_title = @"Open Recent";
[file_menu addItem:MenuItem(open_recent_title)];
NSString* close_all_title = @"Close All";
[file_menu
addItem:MenuItem(close_all_title, @"W", NSEventModifierFlagCommand)];
NSMenu* open_recent_menu = Menu(open_recent_title);
NSMenuItem* open_recent_menu_item =
[file_menu itemWithTitle:[open_recent_menu title]];
[open_recent_menu_item setSubmenu:open_recent_menu];
[open_recent_menu addItem:MenuItem(@"Mock up")];
[open_recent_menu addItem:MenuItem(@"Preview-1")];
[open_recent_menu addItem:MenuItem(@"Scratchpad")];
[open_recent_menu addItem:[NSMenuItem separatorItem]];
NSString* clear_menu_title = @"Clear Menu";
[open_recent_menu
addItem:MenuItem(clear_menu_title, @"c",
NSEventModifierFlagCommand | NSEventModifierFlagControl |
NSEventModifierFlagOption |
NSEventModifierFlagFunction)];
NSEvent* event = KeyEvent(NSEventModifierFlagCommand, @"o");
EXPECT_EQ([file_menu itemWithTitle:open_title],
[main_menu cr_menuItemForKeyEquivalentEvent:event]);
event = KeyEvent(NSEventModifierFlagCommand, @"W");
EXPECT_EQ([file_menu itemWithTitle:close_all_title],
[main_menu cr_menuItemForKeyEquivalentEvent:event]);
event = KeyEvent(NSEventModifierFlagCommand | NSEventModifierFlagControl |
NSEventModifierFlagOption | NSEventModifierFlagFunction,
@"c");
EXPECT_EQ([open_recent_menu itemWithTitle:clear_menu_title],
[main_menu cr_menuItemForKeyEquivalentEvent:event]);
event = KeyEvent(NSEventModifierFlagCommand, @"g");
EXPECT_EQ(nil, [main_menu cr_menuItemForKeyEquivalentEvent:event]);
}
// Tests that a set pre-search block is executed during calls to
// -[NSMenu cr_menuItemForKeyEquivalentEvent:].
TEST(NSMenuAdditionsTest, TestPreSearchBlock) {
__block bool block_was_called = false;
[NSMenu cr_setMenuItemForKeyEquivalentEventPreSearchBlock:^{
block_was_called = true;
}];
NSMenu* main_menu = Menu(@"Main Menu");
NSEvent* event = KeyEvent(NSEventModifierFlagCommand, @"c");
[main_menu cr_menuItemForKeyEquivalentEvent:event];
EXPECT_TRUE(block_was_called);
// Setting the block again should cause a crash (the API only supports a
// single pre-search block).
EXPECT_CHECK_DEATH(
[NSMenu cr_setMenuItemForKeyEquivalentEventPreSearchBlock:^{
block_was_called = true;
}]);
}
// Tests that +flashMenuForChromeCommand: can locate a menu item with a
// particular Chrome command (tag) and that it correctly restores the menu item
// after the flash.
TEST(NSMenuAdditionsTest, TestLocateMenuItemWithTag) {
NSMenu* orig_main_menu = [NSApp mainMenu];
NSMenu* main_menu = Menu(@"Main Menu");
[NSApp setMainMenu:main_menu];
[main_menu addItem:MenuItem(@"App")];
NSString* file_title = @"File";
[main_menu addItem:MenuItem(file_title)];
[main_menu addItem:MenuItem(@"Edit")];
[main_menu addItem:MenuItem(@"Window")];
[main_menu addItem:MenuItem(@"Help")];
NSMenu* file_menu = Menu(file_title);
NSMenuItem* file_menu_item = [main_menu itemWithTitle:[file_menu title]];
[file_menu_item setSubmenu:file_menu];
[file_menu addItem:MenuItem(@"New")];
NSString* open_title = @"Open";
[file_menu addItem:MenuItem(open_title, @"o", NSEventModifierFlagCommand)];
NSString* open_recent_title = @"Open Recent";
[file_menu addItem:MenuItem(open_recent_title)];
NSString* close_tab_title = @"Close Tab";
[file_menu
addItem:MenuItem(close_tab_title, @"W", NSEventModifierFlagCommand)];
NSMenuItem* close_item = [file_menu itemWithTitle:close_tab_title];
NSMenu* open_recent_menu = Menu(open_recent_title);
NSMenuItem* open_recent_menu_item =
[file_menu itemWithTitle:[open_recent_menu title]];
[open_recent_menu_item setSubmenu:open_recent_menu];
[open_recent_menu addItem:MenuItem(@"Mock up")];
[open_recent_menu addItem:MenuItem(@"Preview-1")];
[open_recent_menu addItem:MenuItem(@"Scratchpad")];
[open_recent_menu addItem:[NSMenuItem separatorItem]];
NSString* clear_menu_title = @"Clear Menu";
[open_recent_menu
addItem:MenuItem(clear_menu_title, @"c",
NSEventModifierFlagCommand | NSEventModifierFlagControl |
NSEventModifierFlagOption |
NSEventModifierFlagFunction)];
NSMenuItem* clear_item = [open_recent_menu itemWithTitle:clear_menu_title];
// Commands have not been set so +flashMenuForChromeCommand: should fail.
const int kCloseTab = 5001;
EXPECT_FALSE([NSMenu flashMenuForChromeCommand:kCloseTab]);
[close_item setTag:kCloseTab];
[close_item setTarget:nil];
const SEL close_action = @selector(close:);
[close_item setAction:close_action];
EXPECT_TRUE([NSMenu flashMenuForChromeCommand:kCloseTab]);
// +flashMenuForChromeCommand: fiddles with the item's target and action. Make
// sure they're properly restored.
EXPECT_EQ([close_item target], nil);
EXPECT_EQ([close_item action], close_action);
const int kClearMenu = 5002;
EXPECT_FALSE([NSMenu flashMenuForChromeCommand:kClearMenu]);
[clear_item setTag:kClearMenu];
[clear_item setTarget:NSApp];
const SEL terminate_action = @selector(terminate:);
[clear_item setAction:terminate_action];
EXPECT_TRUE([NSMenu flashMenuForChromeCommand:kClearMenu]);
EXPECT_EQ([clear_item target], NSApp);
EXPECT_EQ([clear_item action], terminate_action);
[NSApp setMainMenu:orig_main_menu];
}