| // Copyright 2023 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/device_notifications/device_status_icon_unittest.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/profiles/profile_attributes_storage.h" |
| #include "chrome/browser/status_icons/status_icon.h" |
| #include "chrome/browser/status_icons/status_icon_menu_model.h" |
| #include "chrome/browser/status_icons/status_tray.h" |
| #include "chrome/test/base/browser_with_test_window_test.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension.h" |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| namespace { |
| |
| std::u16string GetExpectedOriginConnectionCountLabel(Profile* profile, |
| const url::Origin& origin, |
| const std::string& name, |
| int connection_count) { |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| if (origin.scheme() == extensions::kExtensionScheme) { |
| if (connection_count == 0) { |
| return base::UTF8ToUTF16(base::StringPrintf( |
| "Extension \"%s\" was accessing devices", name.c_str())); |
| } |
| return base::UTF8ToUTF16(base::StringPrintf( |
| "Extension \"%s\" is accessing %d %s", name.c_str(), connection_count, |
| (connection_count <= 1 ? "device" : "devices"))); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| NOTREACHED(); |
| } |
| |
| class MockStatusIcon : public StatusIcon { |
| public: |
| explicit MockStatusIcon(const std::u16string& tool_tip) |
| : tool_tip_(tool_tip) {} |
| void SetImage(const gfx::ImageSkia& image) override {} |
| void SetToolTip(const std::u16string& tool_tip) override { |
| tool_tip_ = tool_tip; |
| } |
| void DisplayBalloon(const gfx::ImageSkia& icon, |
| const std::u16string& title, |
| const std::u16string& contents, |
| const message_center::NotifierId& notifier_id) override {} |
| void UpdatePlatformContextMenu(StatusIconMenuModel* menu) override { |
| menu_item_ = menu; |
| } |
| const std::u16string& tool_tip() const { return tool_tip_; } |
| StatusIconMenuModel* menu_item() const { return menu_item_; } |
| |
| private: |
| raw_ptr<StatusIconMenuModel> menu_item_ = nullptr; |
| std::u16string tool_tip_; |
| }; |
| |
| class MockStatusTray : public StatusTray { |
| public: |
| std::unique_ptr<StatusIcon> CreatePlatformStatusIcon( |
| StatusIconType type, |
| const gfx::ImageSkia& image, |
| const std::u16string& tool_tip) override { |
| return std::make_unique<MockStatusIcon>(tool_tip); |
| } |
| |
| const StatusIcons& GetStatusIconsForTest() const { return status_icons(); } |
| }; |
| |
| } // namespace |
| |
| DeviceStatusIconTestBase::DeviceStatusIconTestBase( |
| std::u16string about_device_label, |
| std::u16string device_content_settings_label) |
| : about_device_label_(std::move(about_device_label)), |
| device_content_settings_label_(std::move(device_content_settings_label)) { |
| } |
| |
| void DeviceStatusIconTestBase::SetUp() { |
| DeviceSystemTrayIconTestBase::SetUp(); |
| TestingBrowserProcess::GetGlobal()->SetStatusTray( |
| std::make_unique<MockStatusTray>()); |
| } |
| |
| void DeviceStatusIconTestBase::TearDown() { |
| DeviceSystemTrayIconTestBase::TearDown(); |
| TestingBrowserProcess::GetGlobal()->SetStatusTray(nullptr); |
| } |
| |
| void DeviceStatusIconTestBase::CheckIcon( |
| const std::vector<DeviceSystemTrayIconTestBase::ProfileItem>& |
| profile_connection_counts) { |
| const auto* status_tray = static_cast<MockStatusTray*>( |
| TestingBrowserProcess::GetGlobal()->status_tray()); |
| ASSERT_TRUE(status_tray); |
| ASSERT_EQ(status_tray->GetStatusIconsForTest().size(), 1u); |
| const auto* status_icon = static_cast<MockStatusIcon*>( |
| status_tray->GetStatusIconsForTest().back().icon.get()); |
| |
| // Sort the |profile_connection_counts| by the address of the profile |
| // pointer. This is necessary because the menu items are created by |
| // iterating through a structure of flat_map<Profile*, bool>. |
| auto sorted_profile_connection_counts = profile_connection_counts; |
| std::ranges::sort(sorted_profile_connection_counts); |
| size_t total_connection_count = 0; |
| size_t total_origin_count = 0; |
| auto* menu_item = status_icon->menu_item(); |
| int menu_idx = 1; |
| int expected_command_id = IDC_DEVICE_SYSTEM_TRAY_ICON_FIRST; |
| CheckClickableMenuItem(menu_item, menu_idx++, about_device_label_, |
| expected_command_id++, /*click=*/false); |
| for (const auto& [profile, origin_items] : sorted_profile_connection_counts) { |
| total_origin_count += origin_items.size(); |
| auto sorted_origin_items = origin_items; |
| // Sort the |origin_items| by origin. This is necessary because the origin |
| // items for each profile in the menu are created by iterating through a |
| // structure of flat_map<url::Origin, ...>. |
| std::ranges::sort(sorted_origin_items); |
| auto* connection_tracker = GetDeviceConnectionTracker(profile, |
| /*create=*/false); |
| ASSERT_TRUE(connection_tracker); |
| CheckSeparatorMenuItem(menu_item, menu_idx++); |
| CheckMenuItemLabel(menu_item, menu_idx++, |
| base::UTF8ToUTF16(profile->GetProfileUserName())); |
| EXPECT_CALL(*GetMockDeviceConnectionTracker(connection_tracker), |
| ShowContentSettingsExceptions()); |
| CheckClickableMenuItem(menu_item, menu_idx++, |
| device_content_settings_label_, |
| expected_command_id++, /*click=*/true); |
| for (const auto& [origin, connection_count, name] : sorted_origin_items) { |
| EXPECT_CALL(*GetMockDeviceConnectionTracker(connection_tracker), |
| ShowSiteSettings(origin)); |
| CheckClickableMenuItem(menu_item, menu_idx++, |
| GetExpectedOriginConnectionCountLabel( |
| profile, origin, name, connection_count), |
| expected_command_id++, /*click=*/true); |
| total_connection_count += connection_count; |
| } |
| } |
| CheckMenuItemLabel( |
| menu_item, 0, |
| GetExpectedTitle(total_origin_count, |
| override_title_total_connection_count_.value_or( |
| total_connection_count))); |
| EXPECT_EQ(status_icon->tool_tip(), |
| GetExpectedTitle(total_origin_count, |
| override_title_total_connection_count_.value_or( |
| total_connection_count))); |
| EXPECT_LE(expected_command_id, IDC_DEVICE_SYSTEM_TRAY_ICON_LAST + 1); |
| } |
| |
| void DeviceStatusIconTestBase::CheckIconHidden() { |
| const auto* status_tray = static_cast<MockStatusTray*>( |
| TestingBrowserProcess::GetGlobal()->status_tray()); |
| ASSERT_TRUE(status_tray); |
| EXPECT_TRUE(status_tray->GetStatusIconsForTest().empty()); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| void DeviceStatusIconTestBase::TestNumCommandIdOverLimitExtensionOrigin() { |
| // There are only 40 command ids available. The test creates a scenario that |
| // will use more than 40 command ids. |
| |
| // Each profile with one origin requires two command IDs (one for "About |
| // Device" and one for "extension is connecting to 1 device"). The below for |
| // loop sets up 19 profiles, which will consume 39 menu items (1 + 19 * 2). |
| size_t num_profiles = 19; |
| std::vector<DeviceSystemTrayIconTestBase::ProfileItem> |
| profile_connection_counts; |
| for (size_t idx = 0; idx < num_profiles; idx++) { |
| std::string profile_name = base::StringPrintf("user%zu", idx); |
| auto* profile = CreateTestingProfile(profile_name); |
| auto extension = CreateExtensionWithName("Test Extension"); |
| AddExtensionToProfile(profile, extension.get()); |
| auto* connection_tracker = |
| GetDeviceConnectionTracker(profile, /*create=*/true); |
| connection_tracker->IncrementConnectionCount(extension->origin()); |
| profile_connection_counts.push_back( |
| {profile, {{extension->origin(), 1, extension->name()}}}); |
| } |
| CheckIcon(profile_connection_counts); |
| |
| // Adding one more profile and it will hit the limit. |
| { |
| std::string profile_name = base::StringPrintf("user%zu", num_profiles); |
| auto* profile = CreateTestingProfile(profile_name); |
| auto extension = CreateExtensionWithName("Test Extension"); |
| AddExtensionToProfile(profile, extension.get()); |
| auto* connection_tracker = GetDeviceConnectionTracker(profile, |
| /*create=*/true); |
| connection_tracker->IncrementConnectionCount(extension->origin()); |
| // The origin connection menu item will not be added because the limit of |
| // connections has been reached. However, icon items are inserted by |
| // iterating over a flat_map<Profile*, bool> structure, so it needs to |
| // identify the last profile by sorting profiles and remove its origin |
| // count. |
| profile_connection_counts.push_back( |
| {profile, {{extension->origin(), 1, extension->name()}}}); |
| std::ranges::sort(profile_connection_counts); |
| profile_connection_counts.back().second.clear(); |
| // The total connection count in the title still captures all of the origins |
| override_title_total_connection_count_ = 20; |
| CheckIcon(profile_connection_counts); |
| } |
| } |
| |
| void DeviceStatusIconTestBase::TestProfileUserNameExtensionOrigin() { |
| std::vector<DeviceSystemTrayIconTestBase::ProfileItem> |
| profile_connection_counts; |
| // std::get<1>(profiles[i]) is the old profile name. |
| // std::get<2>(profiles[i]) is the new profile name. |
| std::vector<std::tuple<Profile*, std::string, std::string>> profiles; |
| for (size_t idx = 0; idx < 2; idx++) { |
| std::string profile_name = base::StringPrintf("user%zu", idx); |
| std::string new_profile_name = base::StringPrintf("user%zu-newname", idx); |
| auto* profile = CreateTestingProfile(profile_name); |
| auto extension = CreateExtensionWithName("Test Extension"); |
| AddExtensionToProfile(profile, extension.get()); |
| auto* connection_tracker = GetDeviceConnectionTracker(profile, |
| /*create=*/true); |
| connection_tracker->IncrementConnectionCount(extension->origin()); |
| profile_connection_counts.push_back( |
| {profile, {{extension->origin(), 1, extension->name()}}}); |
| profiles.emplace_back(profile, profile_name, new_profile_name); |
| } |
| CheckIcon(profile_connection_counts); |
| |
| const auto* status_tray = static_cast<MockStatusTray*>( |
| TestingBrowserProcess::GetGlobal()->status_tray()); |
| ASSERT_TRUE(status_tray); |
| ASSERT_EQ(status_tray->GetStatusIconsForTest().size(), 1u); |
| const auto* status_icon = static_cast<MockStatusIcon*>( |
| status_tray->GetStatusIconsForTest().back().icon.get()); |
| |
| // Sort the |profiles| by the address of the profile pointer. This is |
| // necessary because the menu items are created by iterating through a |
| // structure of flat_map<Profile*, bool>. |
| std::ranges::sort(profiles); |
| |
| // The below is status icon items layout, profile1 name is on [3] and profile2 |
| // name is on [7]. |
| // --------------------------------------------------- |
| // [0]|Google Chrome is accessing Device device(s) | |
| // [1]|About Device device | |
| // [2]|---------------Separator----------------------| |
| // [3]|Profile1 name | |
| // [4]|Device Content Setting for Profile1 | |
| // [5]|Extension name | |
| // [6]|---------------Separator----------------------| |
| // [7]|Profile2 name | |
| // [8]|Device Content Setting for Profile2 | |
| // [9]|Extension name | |
| // --------------------------------------------------- |
| |
| int profile_position1 = 3; |
| int profile_position2 = 7; |
| |
| // Check the current profile names. |
| { |
| auto* menu_item = status_icon->menu_item(); |
| CheckMenuItemLabel( |
| menu_item, profile_position1, |
| base::UTF8ToUTF16(std::get<0>(profiles[0])->GetProfileUserName())); |
| CheckMenuItemLabel( |
| menu_item, profile_position2, |
| base::UTF8ToUTF16(std::get<0>(profiles[1])->GetProfileUserName())); |
| } |
| |
| // Change the first profile name. |
| { |
| profile_manager() |
| ->profile_attributes_storage() |
| ->GetProfileAttributesWithPath(std::get<0>(profiles[0])->GetPath()) |
| ->SetLocalProfileName(base::UTF8ToUTF16(std::get<2>(profiles[0])), |
| /*is_default_name*/ false); |
| |
| auto* menu_item = status_icon->menu_item(); |
| CheckMenuItemLabel(menu_item, profile_position1, |
| base::UTF8ToUTF16(std::get<2>(profiles[0]))); |
| CheckMenuItemLabel(menu_item, profile_position2, |
| base::UTF8ToUTF16(std::get<1>(profiles[1]))); |
| } |
| |
| // Change the second profile name. |
| { |
| profile_manager() |
| ->profile_attributes_storage() |
| ->GetProfileAttributesWithPath(std::get<0>(profiles[1])->GetPath()) |
| ->SetLocalProfileName(base::UTF8ToUTF16(std::get<2>(profiles[1])), |
| /*is_default_name*/ false); |
| |
| auto* menu_item = status_icon->menu_item(); |
| CheckMenuItemLabel(menu_item, profile_position1, |
| base::UTF8ToUTF16(std::get<2>(profiles[0]))); |
| CheckMenuItemLabel(menu_item, profile_position2, |
| base::UTF8ToUTF16(std::get<2>(profiles[1]))); |
| } |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| void DeviceStatusIconTestBase::CheckSeparatorMenuItem( |
| StatusIconMenuModel* menu_item, |
| size_t menu_idx) { |
| ASSERT_LT(menu_idx, menu_item->GetItemCount()); |
| EXPECT_EQ(menu_item->GetSeparatorTypeAt(menu_idx), ui::NORMAL_SEPARATOR); |
| } |
| |
| void DeviceStatusIconTestBase::CheckMenuItemLabel( |
| StatusIconMenuModel* menu_item, |
| size_t menu_idx, |
| std::u16string label) { |
| ASSERT_LT(menu_idx, menu_item->GetItemCount()); |
| EXPECT_EQ(menu_item->GetLabelAt(menu_idx), label); |
| } |
| |
| void DeviceStatusIconTestBase::CheckClickableMenuItem( |
| StatusIconMenuModel* menu_item, |
| size_t menu_idx, |
| std::u16string label, |
| int command_id, |
| bool click) { |
| CheckMenuItemLabel(menu_item, menu_idx, label); |
| ASSERT_LT(menu_idx, menu_item->GetItemCount()); |
| EXPECT_EQ(menu_item->GetCommandIdAt(menu_idx), command_id); |
| if (click) { |
| menu_item->ActivatedAt(menu_idx); |
| } |
| } |