chromium/ui/base/cocoa/nsmenu_additions_unittest.mm

// 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];
}