| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/toolbar/app_menu.h" |
| |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/auto_reset.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/time/time.h" |
| #include "base/time/time_override.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "build/branding_buildflags.h" |
| #include "build/build_config.h" |
| #include "build/buildflag.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/profiles/profile_test_util.h" |
| #include "chrome/browser/sessions/tab_restore_service_factory.h" |
| #include "chrome/browser/sessions/tab_restore_service_load_waiter.h" |
| #include "chrome/browser/signin/identity_manager_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/browser/ui/hats/mock_trust_safety_sentiment_service.h" |
| #include "chrome/browser/ui/hats/trust_safety_sentiment_service_factory.h" |
| #include "chrome/browser/ui/safety_hub/safety_hub_hats_service.h" |
| #include "chrome/browser/ui/safety_hub/safety_hub_hats_service_factory.h" |
| #include "chrome/browser/ui/safety_hub/safety_hub_test_util.h" |
| #include "chrome/browser/ui/test/test_browser_ui.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/browser/ui/views/frame/app_menu_button_observer.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/toolbar/browser_app_menu_button.h" |
| #include "chrome/browser/ui/views/toolbar/toolbar_view.h" |
| #include "chrome/browser/upgrade_detector/upgrade_detector.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/commerce/core/commerce_feature_list.h" |
| #include "components/password_manager/core/common/password_manager_features.h" |
| #include "components/signin/public/base/signin_pref_names.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/signin/public/identity_manager/identity_test_utils.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/controls/menu/menu_item_view.h" |
| #include "ui/views/controls/menu/menu_runner.h" |
| #include "ui/views/controls/menu/menu_scroll_view_container.h" |
| #include "ui/views/controls/menu/submenu_view.h" |
| |
| namespace { |
| |
| class AppMenuBrowserTest : public UiBrowserTest { |
| public: |
| AppMenuBrowserTest() { |
| // Disable the comparison tables submenu. |
| // TODO(crbug.com/429347589): Clean up and update test to work by triggering |
| // disruptive notification revocation (or other SH feature). |
| scoped_feature_list_.InitWithFeatures( |
| {}, /*disabled_features=*/{ |
| features::kSafetyHubDisruptiveNotificationRevocation, |
| commerce::kProductSpecifications}); |
| } |
| |
| // UiBrowserTest: |
| void ShowUi(const std::string& name) override; |
| bool VerifyUi() override; |
| void WaitForUserDismissal() override; |
| |
| protected: |
| // Changes the return value of `browser()` as long as the returned object is |
| // alive. This resetting behavior is necessary to null `browser_` before its |
| // destruction, lest the allocator complain about dangling refs. |
| [[nodiscard]] base::AutoReset<raw_ptr<Browser>> SetBrowser(Browser* browser) { |
| return base::AutoReset<raw_ptr<Browser>>(&browser_, browser); |
| } |
| |
| Browser* browser() { |
| return browser_ ? browser_.get() : UiBrowserTest::browser(); |
| } |
| |
| BrowserAppMenuButton* menu_button() { |
| return BrowserView::GetBrowserViewForBrowser(browser()) |
| ->toolbar() |
| ->app_menu_button(); |
| } |
| |
| private: |
| raw_ptr<Browser> browser_ = nullptr; |
| std::optional<int> command_id_; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| void AppMenuBrowserTest::ShowUi(const std::string& name) { |
| // Include mnemonics in screenshots so that we detect changes to them. |
| menu_button()->ShowMenu(views::MenuRunner::SHOULD_SHOW_MNEMONICS); |
| |
| if (base::StartsWith(name, "main")) { |
| return; |
| } |
| |
| constexpr auto kSubmenus = base::MakeFixedFlatMap<std::string_view, int>({ |
| // Submenus present in all versions. |
| {"history", IDC_RECENT_TABS_MENU}, |
| {"bookmarks", IDC_BOOKMARKS_MENU}, |
| {"bookmarks_comparison_tables", IDC_BOOKMARKS_MENU}, |
| {"more_tools", IDC_MORE_TOOLS_MENU}, |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| {"help", IDC_HELP_MENU}, |
| #endif |
| |
| // Submenus only present after Chrome Refresh. |
| {"passwords_and_autofill", IDC_PASSWORDS_AND_AUTOFILL_MENU}, |
| {"reading_list", IDC_READING_LIST_MENU}, // Inside the bookmarks menu. |
| {"extensions", IDC_EXTENSIONS_SUBMENU}, |
| {"find_and_edit", IDC_FIND_AND_EDIT_MENU}, |
| {"save_and_share", IDC_SAVE_AND_SHARE_MENU}, |
| {"profile_menu_in_app_menu_signed_out", IDC_PROFILE_MENU_IN_APP_MENU}, |
| {"profile_menu_in_app_menu_signed_in", IDC_PROFILE_MENU_IN_APP_MENU}, |
| {"profile_menu_in_app_menu_signin_not_allowed", |
| IDC_PROFILE_MENU_IN_APP_MENU}, |
| }); |
| const auto id_entry = kSubmenus.find(name); |
| if (id_entry == kSubmenus.end()) { |
| ADD_FAILURE() << "Unknown submenu " << name; |
| return; |
| } |
| command_id_ = id_entry->second; |
| views::MenuItemView* const menu_root = |
| menu_button()->app_menu()->root_menu_item(); |
| menu_root->GetMenuController()->SelectItemAndOpenSubmenu( |
| menu_root->GetMenuItemByID(command_id_.value())); |
| } |
| |
| bool AppMenuBrowserTest::VerifyUi() { |
| if (!menu_button()->IsMenuShowing()) { |
| return false; |
| } |
| views::MenuItemView* menu_item = menu_button()->app_menu()->root_menu_item(); |
| if (command_id_.has_value()) { |
| menu_item = menu_item->GetMenuItemByID(command_id_.value()); |
| } |
| if (!menu_item->SubmenuIsShowing()) { |
| return false; |
| } |
| |
| const auto* const test_info = |
| testing::UnitTest::GetInstance()->current_test_info(); |
| return VerifyPixelUi(menu_item->GetSubmenu()->GetScrollViewContainer(), |
| test_info->test_suite_name(), |
| test_info->name()) != ui::test::ActionResult::kFailed; |
| } |
| |
| void AppMenuBrowserTest::WaitForUserDismissal() { |
| base::RunLoop run_loop; |
| |
| class CloseWaiter : public AppMenuButtonObserver { |
| public: |
| explicit CloseWaiter(base::RepeatingClosure quit_closure) |
| : quit_closure_(std::move(quit_closure)) {} |
| |
| // AppMenuButtonObserver: |
| void AppMenuClosed() override { quit_closure_.Run(); } |
| |
| private: |
| const base::RepeatingClosure quit_closure_; |
| } waiter(run_loop.QuitClosure()); |
| |
| base::ScopedObservation<BrowserAppMenuButton, CloseWaiter> observation( |
| &waiter); |
| observation.Observe(menu_button()); |
| |
| run_loop.Run(); |
| } |
| |
| // This test shows the app-menu with a closed window added to the |
| // TabRestoreService. This is a regression test to ensure menu code handles this |
| // properly (this was triggering a crash in AppMenu where it was trying to make |
| // use of RecentTabsMenuModelDelegate before created). See |
| // https://crbug.com/1249741 for more. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, ShowWithRecentlyClosedWindow) { |
| // Create an additional browser, close it, and ensure it is added to the |
| // TabRestoreService. |
| sessions::TabRestoreService* tab_restore_service = |
| TabRestoreServiceFactory::GetForProfile(browser()->profile()); |
| TabRestoreServiceLoadWaiter tab_restore_service_load_waiter( |
| tab_restore_service); |
| tab_restore_service_load_waiter.Wait(); |
| Browser* second_browser = CreateBrowser(browser()->profile()); |
| content::WebContents* new_contents = chrome::AddSelectedTabWithURL( |
| second_browser, |
| ui_test_utils::GetTestUrl(base::FilePath(), |
| base::FilePath().AppendASCII("simple.html")), |
| ui::PAGE_TRANSITION_TYPED); |
| EXPECT_TRUE(content::WaitForLoadStop(new_contents)); |
| chrome::CloseWindow(second_browser); |
| ui_test_utils::WaitForBrowserToClose(second_browser); |
| EXPECT_TRUE(base::Contains(tab_restore_service->entries(), |
| sessions::tab_restore::Type::WINDOW, |
| &sessions::tab_restore::Entry::type)); |
| |
| // Show the AppMenu. |
| menu_button()->ShowMenu(views::MenuRunner::NO_FLAGS); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, ExpandCollapse) { |
| EXPECT_FALSE(menu_button()->IsMenuShowing()); |
| |
| ui::AXActionData action_data; |
| action_data.action = ax::mojom::Action::kExpand; |
| menu_button()->HandleAccessibleAction(action_data); |
| EXPECT_TRUE(menu_button()->IsMenuShowing()); |
| action_data.action = ax::mojom::Action::kCollapse; |
| menu_button()->HandleAccessibleAction(action_data); |
| EXPECT_FALSE(menu_button()->IsMenuShowing()); |
| } |
| |
| // There should be at least one subtest below for every distinct submenu of the |
| // app menu; note that the "main" menu also counts as a submenu. More tests are |
| // needed if a submenu can have distinct appearances that should all be tested, |
| // e.g. if different profile data alters the menu appearance. |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, InvokeUi_main) { |
| ShowAndVerifyUi(); |
| } |
| |
| // TODO(crbug.com/343368219): Flaky on Windows 10 x64 builds. |
| #if BUILDFLAG(IS_WIN) && defined(ARCH_CPU_X86_64) |
| #define MAYBE_InvokeUi_main_upgrade_available \ |
| DISABLED_InvokeUi_main_upgrade_available |
| #else |
| #define MAYBE_InvokeUi_main_upgrade_available InvokeUi_main_upgrade_available |
| #endif |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, |
| MAYBE_InvokeUi_main_upgrade_available) { |
| UpgradeDetector::GetInstance()->set_upgrade_notification_stage_for_testing( |
| UpgradeDetector::UPGRADE_ANNOYANCE_CRITICAL); |
| UpgradeDetector::GetInstance()->NotifyUpgradeForTesting(); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, InvokeUi_main_guest) { |
| // TODO(crbug.com/40899974): ChromeOS specific profile logic still needs to be |
| // updated, setup this test for a Guest user session with appropriate command |
| // line switches afterwards. |
| #if !BUILDFLAG(IS_CHROMEOS) |
| auto browser_resetter = SetBrowser(CreateGuestBrowser()); |
| ShowAndVerifyUi(); |
| #endif |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, InvokeUi_main_incognito) { |
| auto browser_resetter = SetBrowser(CreateIncognitoBrowser()); |
| ShowAndVerifyUi(); |
| } |
| |
| // TODO(crbug.com/375132024): Re-enable test. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, DISABLED_InvokeUi_history) { |
| ShowAndVerifyUi(); |
| } |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, InvokeUi_bookmarks) { |
| ShowAndVerifyUi(); |
| } |
| // Flaky b/40261456 |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, DISABLED_InvokeUi_more_tools) { |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, AppMenuViewAccessibleProperties) { |
| menu_button()->ShowMenu(views::MenuRunner::SHOULD_SHOW_MNEMONICS); |
| auto* app_menu_view = menu_button()->app_menu()->GetZoomAppMenuViewForTest(); |
| ui::AXNodeData data; |
| |
| ASSERT_TRUE(app_menu_view); |
| app_menu_view->GetViewAccessibility().GetAccessibleNodeData(&data); |
| EXPECT_EQ(data.role, ax::mojom::Role::kMenu); |
| } |
| |
| #if BUILDFLAG(GOOGLE_CHROME_BRANDING) |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, InvokeUi_help) { |
| ShowAndVerifyUi(); |
| } |
| #endif |
| |
| // TODO(crbug.com/375132024): Re-enable test. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, |
| DISABLED_InvokeUi_passwords_and_autofill) { |
| ShowAndVerifyUi(); |
| } |
| |
| // TODO(crbug.com/375132024): Re-enable test. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, DISABLED_InvokeUi_reading_list) { |
| ShowAndVerifyUi(); |
| } |
| |
| // TODO(crbug.com/375132024): Re-enable test. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, DISABLED_InvokeUi_extensions) { |
| ShowAndVerifyUi(); |
| } |
| |
| // TODO(crbug.com/375132024): Re-enable test. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, DISABLED_InvokeUi_find_and_edit) { |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, InvokeUi_save_and_share) { |
| ShowAndVerifyUi(); |
| } |
| |
| #if !BUILDFLAG(IS_CHROMEOS) |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, InvokeUi_main_profile_signed_in) { |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(browser()->profile()); |
| signin::MakePrimaryAccountAvailable(identity_manager, "user@example.com", |
| signin::ConsentLevel::kSignin); |
| ShowAndVerifyUi(); |
| } |
| |
| // TODO(crbug.com/375132024): Re-enable test. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, |
| DISABLED_InvokeUi_profile_menu_in_app_menu_signed_out) { |
| ProfileManager* profile_manager = g_browser_process->profile_manager(); |
| base::FilePath new_path = profile_manager->GenerateNextProfileDirectoryPath(); |
| profiles::testing::CreateProfileSync(profile_manager, new_path); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, |
| InvokeUi_profile_menu_in_app_menu_signed_in) { |
| signin::IdentityManager* identity_manager = |
| IdentityManagerFactory::GetForProfile(browser()->profile()); |
| signin::MakePrimaryAccountAvailable(identity_manager, "user@example.com", |
| signin::ConsentLevel::kSignin); |
| ShowAndVerifyUi(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, |
| InvokeUi_profile_menu_in_app_menu_signin_not_allowed) { |
| browser()->profile()->GetPrefs()->SetBoolean(prefs::kSigninAllowed, false); |
| ShowAndVerifyUi(); |
| } |
| |
| #endif |
| |
| // Test case for the comparison table submenu under bookmarks. Only appears when |
| // the Compare feature is enabled. |
| class AppMenuBrowserTestCompareOnly : public AppMenuBrowserTest { |
| public: |
| AppMenuBrowserTestCompareOnly() { |
| scoped_feature_list_.InitAndEnableFeature(commerce::kProductSpecifications); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTestCompareOnly, |
| InvokeUi_bookmarks_comparison_tables) { |
| ShowAndVerifyUi(); |
| } |
| |
| // Test case for Safety Hub notification. |
| IN_PROC_BROWSER_TEST_F(AppMenuBrowserTest, Safety_Hub_shown_notification) { |
| auto* mock_sentiment_service = static_cast<MockTrustSafetySentimentService*>( |
| TrustSafetySentimentServiceFactory::GetInstance() |
| ->SetTestingFactoryAndUse( |
| browser()->profile(), |
| base::BindRepeating(&BuildMockTrustSafetySentimentService))); |
| safety_hub_test_util::RunUntilPasswordCheckCompleted(browser()->profile()); |
| safety_hub_test_util::GenerateSafetyHubMenuNotification(browser()->profile()); |
| menu_button()->ShowMenu(views::MenuRunner::SHOULD_SHOW_MNEMONICS); |
| // Set the elapsed timer of the menu to start 10 seconds ago. |
| { |
| base::subtle::ScopedTimeClockOverrides override( |
| /*time_override=*/ |
| nullptr, |
| /*time_ticks_override=*/ |
| []() { |
| return base::subtle::TimeTicksNowIgnoringOverride() - |
| base::Seconds(10); |
| }, |
| /*thread_ticks_override=*/nullptr); |
| menu_button()->SetMenuTimerForTesting(base::ElapsedTimer()); |
| } |
| EXPECT_CALL( |
| *mock_sentiment_service, |
| TriggerSafetyHubSurvey( |
| TrustSafetySentimentService::FeatureArea::kSafetyHubNotification, |
| testing::_)); |
| menu_button()->CloseMenu(); |
| } |
| } // namespace |