| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/task_environment.h" |
| #include "chrome/browser/favicon/favicon_utils.h" |
| #include "chrome/browser/ui/cocoa/tab_menu_bridge.h" |
| #include "chrome/browser/ui/tab_ui_helper.h" |
| #include "chrome/browser/ui/tabs/tab_enums.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "content/public/test/test_renderer_host.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "testing/gtest_mac.h" |
| |
| constexpr int kStaticItemCount = 4; |
| |
| class TabStripModelUiHelperDelegate : public TestTabStripModelDelegate { |
| public: |
| void WillAddWebContents(content::WebContents* contents) override { |
| TestTabStripModelDelegate::WillAddWebContents(contents); |
| |
| favicon::CreateContentFaviconDriverForWebContents(contents); |
| TabUIHelper::CreateForWebContents(contents); |
| } |
| }; |
| |
| class TabMenuBridgeTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| profile_ = std::make_unique<TestingProfile>(); |
| rvh_test_enabler_ = std::make_unique<content::RenderViewHostTestEnabler>(); |
| delegate_ = std::make_unique<TabStripModelUiHelperDelegate>(); |
| model_ = std::make_unique<TabStripModel>(delegate_.get(), profile_.get()); |
| menu_root_ = ItemWithTitle(@"Tab"); |
| menu_ = [[NSMenu alloc] initWithTitle:@"Tab"]; |
| menu_root_.submenu = menu_; |
| |
| AddStaticItems(menu_); |
| } |
| |
| void TearDown() override { model_->CloseAllTabs(); } |
| |
| NSMenuItem* menu_root() { return menu_root_; } |
| NSMenu* menu() { return menu_; } |
| TabStripModel* model() { return model_.get(); } |
| TabStripModelDelegate* delegate() { return delegate_.get(); } |
| |
| void AddStaticItems(NSMenu* menu) { |
| [menu_ addItem:ItemWithTitle(@"Static 1")]; |
| [menu_ addItem:ItemWithTitle(@"Static 2")]; |
| [menu_ addItem:ItemWithTitle(@"Static 3")]; |
| [menu_ addItem:SeparatorItem()]; |
| } |
| |
| std::unique_ptr<content::WebContents> CreateWebContents( |
| const std::string& title) { |
| std::unique_ptr<content::WebContents> contents = |
| content::WebContentsTester::CreateTestWebContents(profile_.get(), |
| nullptr); |
| content::WebContentsTester::For(contents.get()) |
| ->SetTitle(base::UTF8ToUTF16(title)); |
| return contents; |
| } |
| |
| content::WebContents* GetModelWebContentsAt(int index) { |
| return model()->GetWebContentsAt(index); |
| } |
| |
| void AddModelTabNamed(const std::string& name) { |
| model()->AppendWebContents(CreateWebContents(name), true); |
| } |
| |
| int ModelIndexForTabNamed(const std::string& name) { |
| std::u16string title16 = base::UTF8ToUTF16(name); |
| for (int i = 0; i < model()->count(); ++i) { |
| if (model()->GetWebContentsAt(i)->GetTitle() == title16) |
| return i; |
| } |
| return -1; |
| } |
| |
| void RemoveModelTabNamed(const std::string& name) { |
| int index = ModelIndexForTabNamed(name); |
| DCHECK(index >= 0); |
| model()->CloseWebContentsAt(index, TabCloseTypes::CLOSE_NONE); |
| } |
| |
| void RenameModelTabNamed(const std::string& old_name, |
| const std::string& new_name) { |
| int index = ModelIndexForTabNamed(old_name); |
| if (index >= 0) { |
| content::WebContents* contents = model()->GetWebContentsAt(index); |
| content::WebContentsTester::For(contents)->SetTitle( |
| base::UTF8ToUTF16(new_name)); |
| // The way WebContentsTester updates the title avoids the usual |
| // notification mechanism for TabStripModel, so manually synthesize the |
| // update notification here. |
| model()->UpdateWebContentsStateAt(index, TabChangeType::kAll); |
| } |
| } |
| |
| void ReplaceModelTabNamed(const std::string& old_name, |
| const std::string& new_name) { |
| int index = ModelIndexForTabNamed(old_name); |
| if (index >= 0) { |
| std::unique_ptr<content::WebContents> old_contents = |
| model()->DiscardWebContentsAt(index, CreateWebContents(new_name)); |
| // Let the old WebContents be destroyed here. |
| } |
| } |
| |
| void ActivateModelTabNamed(const std::string& name) { |
| int index = ModelIndexForTabNamed(name); |
| DCHECK(index >= 0); |
| model()->ActivateTabAt(index); |
| } |
| |
| NSMenuItem* MenuItemForTabNamed(const std::string& name) { |
| return [menu() itemWithTitle:base::SysUTF8ToNSString(name)]; |
| } |
| |
| void ExpectDynamicTabsInMenuAre(const std::vector<std::string>& titles) { |
| std::vector<std::string> actual_titles; |
| for (int i = kStaticItemCount; i < menu().numberOfItems; ++i) { |
| actual_titles.push_back( |
| base::SysNSStringToUTF8([menu() itemAtIndex:i].title)); |
| } |
| |
| ASSERT_EQ(actual_titles.size(), titles.size()); |
| for (int i = 0; i < static_cast<int>(titles.size()); ++i) |
| EXPECT_EQ(actual_titles[i], titles[i]); |
| } |
| |
| std::string ActiveTabName() { |
| return base::UTF16ToUTF8(model()->GetActiveWebContents()->GetTitle()); |
| } |
| |
| void ExpectActiveMenuItemNameIs(const std::string& name) { |
| std::vector<std::string> active_items; |
| // Check the static items too, to make sure none of them are checked. |
| for (int i = 0; i < menu().numberOfItems; ++i) { |
| NSMenuItem* item = [menu() itemAtIndex:i]; |
| if (item.state == NSControlStateValueOn) |
| active_items.push_back(base::SysNSStringToUTF8(item.title)); |
| } |
| |
| ASSERT_EQ(active_items.size(), 1u); |
| EXPECT_EQ(name, active_items[0]); |
| } |
| |
| TestingProfile* profile() { return profile_.get(); } |
| |
| private: |
| NSMenuItem* ItemWithTitle(NSString* title) { |
| return [[NSMenuItem alloc] initWithTitle:title |
| action:nil |
| keyEquivalent:@""]; |
| } |
| |
| NSMenuItem* SeparatorItem() { return [NSMenuItem separatorItem]; } |
| |
| content::BrowserTaskEnvironment task_environment_; |
| |
| std::unique_ptr<TestingProfile> profile_; |
| std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_; |
| std::unique_ptr<TabStripModelUiHelperDelegate> delegate_; |
| std::unique_ptr<TabStripModel> model_; |
| NSMenuItem* __strong menu_root_; |
| NSMenu* __strong menu_; |
| }; |
| |
| TEST_F(TabMenuBridgeTest, CreatesBlankMenu) { |
| TabMenuBridge bridge(model(), menu_root()); |
| bridge.BuildMenu(); |
| EXPECT_EQ(menu().numberOfItems, kStaticItemCount); |
| ExpectDynamicTabsInMenuAre({}); |
| } |
| |
| TEST_F(TabMenuBridgeTest, TracksModelUpdates) { |
| TabMenuBridge bridge(model(), menu_root()); |
| bridge.BuildMenu(); |
| |
| AddModelTabNamed("Tab 1"); |
| AddModelTabNamed("Tab 2"); |
| AddModelTabNamed("Tab 3"); |
| ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"}); |
| |
| RemoveModelTabNamed("Tab 2"); |
| ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 3"}); |
| |
| AddModelTabNamed("Tab 2"); |
| ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 3", "Tab 2"}); |
| |
| RenameModelTabNamed("Tab 1", "Tab 4"); |
| ExpectDynamicTabsInMenuAre({"Tab 4", "Tab 3", "Tab 2"}); |
| |
| content::WebContents* old_contents = GetModelWebContentsAt(0); |
| ReplaceModelTabNamed("Tab 4", "Tab 5"); |
| ASSERT_NE(GetModelWebContentsAt(0), old_contents); |
| ExpectDynamicTabsInMenuAre({"Tab 5", "Tab 3", "Tab 2"}); |
| } |
| |
| // Tests that dynamic menu items added by the bridge are removed on |
| // bridge destruction. |
| TEST_F(TabMenuBridgeTest, RemoveDynamicMenuItemsOnDestruct) { |
| std::unique_ptr<TabMenuBridge> bridge = |
| std::make_unique<TabMenuBridge>(model(), menu_root()); |
| bridge->BuildMenu(); |
| |
| AddModelTabNamed("Tab 1"); |
| AddModelTabNamed("Tab 2"); |
| AddModelTabNamed("Tab 3"); |
| ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"}); |
| |
| bridge.reset(); |
| |
| ExpectDynamicTabsInMenuAre({}); |
| } |
| |
| TEST_F(TabMenuBridgeTest, ClickingMenuActivatesTab) { |
| TabMenuBridge bridge(model(), menu_root()); |
| bridge.BuildMenu(); |
| |
| AddModelTabNamed("Tab 1"); |
| AddModelTabNamed("Tab 2"); |
| EXPECT_EQ(ActiveTabName(), "Tab 2"); |
| |
| NSMenuItem* tab1_item = MenuItemForTabNamed("Tab 1"); |
| |
| // Don't go through NSMenuItem's normal click-dispatching machinery here - it |
| // would add flake potential to this test without improving coverage. Instead, |
| // call straight through to the C++-side callback: |
| bridge.OnDynamicItemChosen(tab1_item); |
| EXPECT_EQ(ActiveTabName(), "Tab 1"); |
| |
| // Activation does not re-order the menu. |
| ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2"}); |
| } |
| |
| // This is a regression test for a bug found during development. Previous |
| // versions of TabMenuBridge had an RAII-like API where creating a TabMenuBridge |
| // would fill in the dynamic menu during construction. Combining this with the |
| // common pattern of: |
| // tab_menu_bridge_ = std::make_unique<TabMenuBridge>(...); |
| // in the presence of an existing tab_menu_bridge_ led to there temporarily |
| // being two TabMenuBridge instances at a time, meaning both of them had their |
| // dynamic menu items installed. This, in turn, confused the menu index logic in |
| // the new TabMenuBridge - it counted the old TabMenuBridge's dynamic items as |
| // static items, and ended up with incorrect indexes. This test exercises that |
| // behavior. |
| TEST_F(TabMenuBridgeTest, SwappingBridgeRecreatesMenu) { |
| auto bridge = std::make_unique<TabMenuBridge>(model(), menu_root()); |
| bridge->BuildMenu(); |
| |
| AddModelTabNamed("Tab 1"); |
| |
| auto model2 = std::make_unique<TabStripModel>(delegate(), profile()); |
| model2->AppendWebContents(CreateWebContents("Tab 2"), true); |
| |
| bridge = std::make_unique<TabMenuBridge>(model2.get(), menu_root()); |
| bridge->BuildMenu(); |
| ExpectDynamicTabsInMenuAre({"Tab 2"}); |
| |
| // Simulate one of the tabs in the model being updated - if the computed |
| // indexes are wrong, this call will DCHECK. |
| model2->UpdateWebContentsStateAt(0, TabChangeType::kAll); |
| |
| model2->CloseAllTabs(); |
| |
| // model2 gets torn down before bridge here, which exercises the code in |
| // TabMenuBridge for handling the TabStripModel being torn down (eg via |
| // browser window close) while the TabMenuBridge still exists. If that code |
| // does not correctly forget about the TabStripModel, this test will crash |
| // here in ASAN builds. |
| } |
| |
| TEST_F(TabMenuBridgeTest, ActiveItemTracksChanges) { |
| TabMenuBridge bridge(model(), menu_root()); |
| bridge.BuildMenu(); |
| |
| AddModelTabNamed("Tab 1"); |
| AddModelTabNamed("Tab 2"); |
| AddModelTabNamed("Tab 3"); |
| ExpectActiveMenuItemNameIs("Tab 3"); |
| |
| ActivateModelTabNamed("Tab 2"); |
| ExpectActiveMenuItemNameIs("Tab 2"); |
| |
| ActivateModelTabNamed("Tab 3"); |
| ExpectActiveMenuItemNameIs("Tab 3"); |
| |
| RemoveModelTabNamed("Tab 1"); |
| ExpectActiveMenuItemNameIs("Tab 3"); |
| } |