chromium/chrome/browser/ui/cocoa/accelerators_cocoa_browsertest.mm

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Cocoa/Cocoa.h>

#include "base/i18n/base_i18n_switches.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest_mac.h"
#include "ui/base/accelerators/platform_accelerator_cocoa.h"
#include "ui/base/l10n/l10n_util_mac.h"
#import "ui/events/keycodes/keyboard_code_conversion_mac.h"

using AcceleratorsCocoaBrowserTest = InProcessBrowserTest;

namespace {

// Adds all NSMenuItems with an accelerator to the array.
void AddAcceleratorItemsToArray(NSMenu* menu, NSMutableArray* array) {
  for (NSMenuItem* item in menu.itemArray) {
    NSMenu* submenu = item.submenu;
    if (submenu)
      AddAcceleratorItemsToArray(submenu, array);

    // If the tag or key equivalent is zero, then either this is a macOS menu
    // item that we don't care about, or it's a chrome accelerator with non
    // standard selector. We don't have an easy way to distinguish between
    // these, so we just ignore them. Also as of macOS Monterey the AppKit
    // adds a tag to the Start Dictation... menu item - skip it as well.
    if (item.tag == 0 || item.keyEquivalent.length == 0 ||
        item.action == @selector(startDictation:))
      continue;

    [array addObject:item];
  }
}

// Checks that the |item|'s modifier mask matches |modifierMask|. The
// NSEventModifierFlagShift may be stored as part of the key equivalent
// (i.e. "a" + NSEventModifierFlagShift => "A").
inline bool MenuItemHasModifierMask(NSMenuItem* item, NSUInteger modifierMask) {
  return modifierMask == item.keyEquivalentModifierMask ||
         (modifierMask & ~NSEventModifierFlagShift) ==
             item.keyEquivalentModifierMask;
}

// Returns the NSMenuItem that has the given keyEquivalent and modifiers, or
// nil.
NSMenuItem* MenuContainsAccelerator(NSMenu* menu,
                                    NSString* key_equivalent,
                                    NSUInteger modifier_mask) {
  for (NSMenuItem* item in menu.itemArray) {
    NSMenu* submenu = item.submenu;
    if (submenu) {
      NSMenuItem* result =
          MenuContainsAccelerator(submenu, key_equivalent, modifier_mask);
      if (result)
        return result;
    }

    if ([item.keyEquivalent isEqual:key_equivalent]) {
      // We don't want to ignore shift for [cmd + shift + tab] and [cmd + tab],
      // which are special.
      if (item.tag == IDC_SELECT_NEXT_TAB ||
          item.tag == IDC_SELECT_PREVIOUS_TAB) {
        if (modifier_mask == item.keyEquivalentModifierMask)
          return item;
        continue;
      }

      if (MenuItemHasModifierMask(item, modifier_mask))
        return item;
    }
  }
  return nil;
}

}  // namespace

class AcceleratorsCocoaBrowserTestRTL : public AcceleratorsCocoaBrowserTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitchASCII(::switches::kForceUIDirection,
                                    ::switches::kForceDirectionRTL);
    command_line->AppendSwitchASCII(::switches::kForceTextDirection,
                                    ::switches::kForceDirectionRTL);
  }
};

// Checks that each NSMenuItem in the main menu has a corresponding accelerator,
// and the keyEquivalent/modifiers match.
IN_PROC_BROWSER_TEST_F(AcceleratorsCocoaBrowserTest,
                       MainMenuAcceleratorsInMapping) {
  NSMenu* menu = [NSApp mainMenu];
  NSMutableArray* array = [NSMutableArray array];
  AddAcceleratorItemsToArray(menu, array);
  AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();

  for (NSMenuItem* item in array) {
    const ui::Accelerator* accelerator =
        keymap->GetAcceleratorForCommand(item.tag);
    EXPECT_TRUE(accelerator);
    if (!accelerator) {
      continue;
    }

    // Get the Cocoa key_equivalent associated with the accelerator.
    KeyEquivalentAndModifierMask* equivalent =
        GetKeyEquivalentAndModifierMaskFromAccelerator(*accelerator);

    // Check that the menu item's keyEquivalent matches the one from the
    // Cocoa accelerator map.
    EXPECT_NSEQ(equivalent.keyEquivalent, item.keyEquivalent);

    // Check that the menu item's modifier mask matches the one stored in the
    // accelerator. Ignore the NSEventModifierFlagShift because it's part of
    // the key equivalent (i.e. "a" + NSEventModifierFlagShift = "A").
    EXPECT_TRUE(MenuItemHasModifierMask(item, equivalent.modifierMask));
  }
}

// Check that each accelerator with a command_id has an associated NSMenuItem
// in the main menu. If the selector is commandDispatch:, then the tag must
// match the command_id.
IN_PROC_BROWSER_TEST_F(AcceleratorsCocoaBrowserTest,
                       MappingAcceleratorsInMainMenu) {
  AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
  // The "Share" menu is dynamically populated.
  NSMenu* mainMenu = NSApp.mainMenu;
  NSMenu* fileMenu = [[mainMenu itemWithTag:IDC_FILE_MENU] submenu];
  NSMenu* shareMenu =
      [[fileMenu itemWithTitle:l10n_util::GetNSString(IDS_SHARE_MAC)] submenu];
  [[shareMenu delegate] menuNeedsUpdate:shareMenu];

  for (auto& it : keymap->accelerators_) {
    KeyEquivalentAndModifierMask* equivalent =
        GetKeyEquivalentAndModifierMaskFromAccelerator(it.second);

    // Check that there exists a corresponding NSMenuItem.
    NSMenuItem* item = MenuContainsAccelerator(
        [NSApp mainMenu], equivalent.keyEquivalent, equivalent.modifierMask);
    EXPECT_TRUE(item);

    // If the menu uses a commandDispatch:, the tag must match the command id!
    // Added an exception for IDC_TOGGLE_FULLSCREEN_TOOLBAR, which conflicts
    // with IDC_PRESENTATION_MODE.
    if (item.action == @selector(commandDispatch:) &&
        item.tag != IDC_TOGGLE_FULLSCREEN_TOOLBAR) {
      EXPECT_EQ(item.tag, it.first);
    }
  }
}

IN_PROC_BROWSER_TEST_F(AcceleratorsCocoaBrowserTestRTL,
                       HistoryAcceleratorsReversedForRTL) {
  AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
  ui::Accelerator history_forward = keymap->accelerators_[IDC_FORWARD];
  ui::Accelerator history_back = keymap->accelerators_[IDC_BACK];

  // In LTR, History -> Forward is VKEY_OEM_6 and Back is VKEY_OEM_4.
  EXPECT_EQ(ui::VKEY_OEM_4, history_forward.key_code());
  EXPECT_EQ(ui::VKEY_OEM_6, history_back.key_code());
}