| // 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. |
| |
| #include "base/memory/ref_counted.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/extensions/api/side_panel/side_panel_api.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/side_panel/extensions/extension_side_panel_coordinator.h" |
| #include "chrome/browser/ui/views/side_panel/extensions/extension_side_panel_manager.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel_coordinator.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel_entry.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel_entry_observer.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel_registry.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel_registry_observer.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/test_utils.h" |
| #include "extensions/browser/api_test_utils.h" |
| #include "extensions/browser/test_image_loader.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "ui/gfx/image/image_unittest_util.h" |
| |
| namespace extensions { |
| namespace { |
| |
| SidePanelEntry::Key GetKey(const ExtensionId& id) { |
| return SidePanelEntry::Key(SidePanelEntry::Id::kExtension, id); |
| } |
| |
| // A class which waits on various SidePanelEntryObserver events. |
| class TestSidePanelEntryWaiter : public SidePanelEntryObserver { |
| public: |
| explicit TestSidePanelEntryWaiter(SidePanelEntry* entry) { |
| side_panel_entry_observation_.Observe(entry); |
| } |
| |
| ~TestSidePanelEntryWaiter() override = default; |
| TestSidePanelEntryWaiter(const TestSidePanelEntryWaiter& other) = delete; |
| TestSidePanelEntryWaiter& operator=(const TestSidePanelEntryWaiter& other) = |
| delete; |
| |
| void WaitForEntryShown() { entry_shown_run_loop_.Run(); } |
| |
| void WaitForIconUpdated() { icon_updated_run_loop_.Run(); } |
| |
| private: |
| void OnEntryShown(SidePanelEntry* entry) override { |
| entry_shown_run_loop_.QuitWhenIdle(); |
| } |
| |
| void OnEntryIconUpdated(SidePanelEntry* entry) override { |
| icon_updated_run_loop_.QuitWhenIdle(); |
| } |
| |
| base::RunLoop entry_shown_run_loop_; |
| base::RunLoop icon_updated_run_loop_; |
| base::ScopedObservation<SidePanelEntry, SidePanelEntryObserver> |
| side_panel_entry_observation_{this}; |
| }; |
| |
| // A class which waits for an extension's SidePanelEntry to be registered and/or |
| // deregistered. |
| class ExtensionSidePanelRegistryWaiter : public SidePanelRegistryObserver { |
| public: |
| explicit ExtensionSidePanelRegistryWaiter(SidePanelRegistry* registry, |
| const ExtensionId& extension_id) |
| : extension_id_(extension_id) { |
| side_panel_registry_observation_.Observe(registry); |
| } |
| |
| ~ExtensionSidePanelRegistryWaiter() override = default; |
| ExtensionSidePanelRegistryWaiter( |
| const ExtensionSidePanelRegistryWaiter& other) = delete; |
| ExtensionSidePanelRegistryWaiter& operator=( |
| const ExtensionSidePanelRegistryWaiter& other) = delete; |
| |
| // Waits until the entry for `extension_id_` is registered. |
| void WaitForRegistration() { registration_run_loop_.Run(); } |
| |
| // Waits until the entry for `extension_id_` is deregistered. |
| void WaitForDeregistration() { deregistration_run_loop_.Run(); } |
| |
| private: |
| // SidePanelRegistryObserver implementation. |
| void OnEntryRegistered(SidePanelEntry* entry) override { |
| if (entry->key() == GetKey(extension_id_)) { |
| registration_run_loop_.QuitWhenIdle(); |
| } |
| } |
| |
| void OnEntryWillDeregister(SidePanelRegistry* registry, |
| SidePanelEntry* entry) override { |
| if (entry->key() == GetKey(extension_id_)) { |
| deregistration_run_loop_.QuitWhenIdle(); |
| } |
| } |
| |
| ExtensionId extension_id_; |
| base::RunLoop registration_run_loop_; |
| base::RunLoop deregistration_run_loop_; |
| base::ScopedObservation<SidePanelRegistry, SidePanelRegistryObserver> |
| side_panel_registry_observation_{this}; |
| }; |
| |
| class ExtensionSidePanelBrowserTest : public ExtensionBrowserTest { |
| public: |
| ExtensionSidePanelBrowserTest() { |
| feature_list_.InitAndEnableFeature( |
| extensions_features::kExtensionSidePanelIntegration); |
| } |
| |
| protected: |
| // Calls chrome.sidePanel.setOptions() for the given `extension`, `path` and |
| // `enabled` and returns when the API call is complete. |
| void RunSetOptions(const Extension& extension, |
| const std::string& path, |
| bool enabled) { |
| auto function = base::MakeRefCounted<SidePanelSetOptionsFunction>(); |
| function->set_extension(&extension); |
| |
| std::string args = |
| base::StringPrintf(R"([{"path":"%s","enabled":%s}])", path.c_str(), |
| enabled ? "true" : "false"); |
| EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile())) |
| << function->GetError(); |
| } |
| |
| SidePanelRegistry* global_registry() { |
| return side_panel_coordinator()->GetGlobalSidePanelRegistry(); |
| } |
| |
| SidePanelCoordinator* side_panel_coordinator() { |
| return BrowserView::GetBrowserViewForBrowser(browser()) |
| ->side_panel_coordinator(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Test that only extensions with side panel content will have a SidePanelEntry |
| // registered. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelBrowserTest, |
| ExtensionEntryVisibleInSidePanel) { |
| // Load two extensions: one with a side panel entry in its manifest and one |
| // without. |
| scoped_refptr<const extensions::Extension> no_side_panel_extension = |
| LoadExtension(test_data_dir_.AppendASCII("common/background_script")); |
| ASSERT_TRUE(no_side_panel_extension); |
| |
| scoped_refptr<const extensions::Extension> side_panel_extension = |
| LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/simple_default")); |
| ASSERT_TRUE(side_panel_extension); |
| |
| // Check that only the extension with the side panel entry in its manifest is |
| // shown as an entry in the global side panel registry. |
| EXPECT_TRUE(global_registry()->GetEntryForKey(SidePanelEntry::Key( |
| SidePanelEntry::Id::kExtension, side_panel_extension->id()))); |
| EXPECT_FALSE(global_registry()->GetEntryForKey(SidePanelEntry::Key( |
| SidePanelEntry::Id::kExtension, no_side_panel_extension->id()))); |
| |
| // Unloading the extension should remove it from the registry. |
| UnloadExtension(side_panel_extension->id()); |
| EXPECT_FALSE(global_registry()->GetEntryForKey(SidePanelEntry::Key( |
| SidePanelEntry::Id::kExtension, side_panel_extension->id()))); |
| } |
| |
| // Test that an extension's view is shown/behaves correctly in the side panel. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelBrowserTest, |
| ExtensionViewVisibleInsideSidePanel) { |
| ExtensionTestMessageListener default_path_listener("default_path"); |
| |
| scoped_refptr<const extensions::Extension> extension = LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/simple_default")); |
| ASSERT_TRUE(extension); |
| |
| SidePanelEntry::Key extension_key = |
| SidePanelEntry::Key(SidePanelEntry::Id::kExtension, extension->id()); |
| SidePanelEntry* extension_entry = |
| global_registry()->GetEntryForKey(extension_key); |
| ASSERT_TRUE(extension_entry); |
| |
| // The key for the extension should be registered, but the side panel isn't |
| // shown yet. |
| EXPECT_FALSE(side_panel_coordinator()->IsSidePanelShowing()); |
| |
| side_panel_coordinator()->Show(extension_key); |
| |
| // Wait until the view in the side panel is active by listening for the |
| // message sent from the view's script. |
| ASSERT_TRUE(default_path_listener.WaitUntilSatisfied()); |
| EXPECT_TRUE(side_panel_coordinator()->IsSidePanelShowing()); |
| |
| // Reset the `default_path_listener`. |
| default_path_listener.Reset(); |
| |
| // Close and reopen the side panel. The extension's view should be recreated. |
| side_panel_coordinator()->Close(); |
| EXPECT_FALSE(side_panel_coordinator()->IsSidePanelShowing()); |
| side_panel_coordinator()->Show(extension_key); |
| |
| ASSERT_TRUE(default_path_listener.WaitUntilSatisfied()); |
| EXPECT_TRUE(side_panel_coordinator()->IsSidePanelShowing()); |
| |
| // Now unload the extension. The key should no longer exist in the global |
| // registry and the side panel should close as a result. |
| UnloadExtension(extension->id()); |
| EXPECT_FALSE(global_registry()->GetEntryForKey(extension_key)); |
| EXPECT_FALSE(side_panel_coordinator()->IsSidePanelShowing()); |
| } |
| |
| // Test that an extension's SidePanelEntry is registered for new browser |
| // windows. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelBrowserTest, MultipleBrowsers) { |
| // Load an extension and verify that its SidePanelEntry is registered. |
| scoped_refptr<const extensions::Extension> extension = LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/simple_default")); |
| ASSERT_TRUE(extension); |
| SidePanelEntry::Key extension_key = |
| SidePanelEntry::Key(SidePanelEntry::Id::kExtension, extension->id()); |
| |
| EXPECT_TRUE(global_registry()->GetEntryForKey(extension_key)); |
| |
| // Open a new browser window. The extension's SidePanelEntry should also be |
| // registered for the new window's global SidePanelRegistry. |
| Browser* second_browser = CreateBrowser(browser()->profile()); |
| SidePanelRegistry* second_global_registry = |
| BrowserView::GetBrowserViewForBrowser(second_browser) |
| ->side_panel_coordinator() |
| ->GetGlobalSidePanelRegistry(); |
| EXPECT_TRUE(second_global_registry->GetEntryForKey(extension_key)); |
| } |
| |
| // Test that if the side panel is closed while the extension's side panel view |
| // is still loading, there will not be a crash. Regression for |
| // crbug.com/1403168. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelBrowserTest, SidePanelQuicklyClosed) { |
| // Load an extension and verify that its SidePanelEntry is registered. |
| scoped_refptr<const extensions::Extension> extension = LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/simple_default")); |
| ASSERT_TRUE(extension); |
| SidePanelEntry::Key extension_key = |
| SidePanelEntry::Key(SidePanelEntry::Id::kExtension, extension->id()); |
| |
| EXPECT_TRUE(global_registry()->GetEntryForKey(extension_key)); |
| EXPECT_FALSE(side_panel_coordinator()->IsSidePanelShowing()); |
| |
| // Quickly open the side panel showing the extension's side panel entry then |
| // close it. The test should not cause any crashes after it is complete. |
| side_panel_coordinator()->Show(extension_key); |
| EXPECT_TRUE(side_panel_coordinator()->IsSidePanelShowing()); |
| side_panel_coordinator()->Close(); |
| } |
| |
| // Test that the extension's side panel entry shows the extension's icon. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelBrowserTest, EntryShowsExtensionIcon) { |
| // Load an extension and verify that its SidePanelEntry is registered. |
| scoped_refptr<const extensions::Extension> extension = LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/simple_default")); |
| ASSERT_TRUE(extension); |
| |
| auto* extension_coordinator = |
| extensions::ExtensionSidePanelManager::GetOrCreateForBrowser(browser()) |
| ->GetExtensionCoordinatorForTesting(extension->id()); |
| |
| SidePanelEntry::Key extension_key = |
| SidePanelEntry::Key(SidePanelEntry::Id::kExtension, extension->id()); |
| SidePanelEntry* extension_entry = |
| global_registry()->GetEntryForKey(extension_key); |
| |
| // At this point, we don't know if the extension's icon has finished loading |
| // or not, since the first icon load is initiated right when the extension |
| // loads. Attempting to wait on OnEntryIconUpdated will hang forever if the |
| // icon has been loaded after setting up the waiter. To ensure the icon is |
| // loaded and the OnEntryIconUpdated event is broadcast, initiate a reload for |
| // the extension's icon manually. |
| { |
| TestSidePanelEntryWaiter icon_updated_waiter(extension_entry); |
| extension_coordinator->LoadExtensionIconForTesting(); |
| icon_updated_waiter.WaitForIconUpdated(); |
| } |
| |
| // Check that the entry's icon bitmap is identical to the bitmap of the |
| // extension's icon scaled down to `extension_misc::EXTENSION_ICON_BITTY`. |
| SkBitmap expected_icon_bitmap = TestImageLoader::LoadAndGetExtensionBitmap( |
| extension.get(), "icon.png", extension_misc::EXTENSION_ICON_BITTY); |
| const SkBitmap& actual_icon_bitmap = |
| *extension_entry->icon().GetImage().ToSkBitmap(); |
| EXPECT_TRUE( |
| gfx::test::AreBitmapsEqual(expected_icon_bitmap, actual_icon_bitmap)); |
| } |
| |
| // Test that sidePanel.setOptions() will register and deregister the extension's |
| // SidePanelEntry when called with enabled: true/false. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelBrowserTest, SetOptions_Enabled) { |
| ExtensionTestMessageListener panel_2_listener("panel_2"); |
| |
| // Load an extension without a default side panel path. |
| scoped_refptr<const extensions::Extension> extension = LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/setoptions_default_tab")); |
| ASSERT_TRUE(extension); |
| SidePanelEntry::Key extension_key = |
| SidePanelEntry::Key(SidePanelEntry::Id::kExtension, extension->id()); |
| EXPECT_FALSE(global_registry()->GetEntryForKey(extension_key)); |
| |
| { |
| // Call setOptions({enabled: true}) and wait for the extension's |
| // SidePanelEntry to be registered. |
| ExtensionSidePanelRegistryWaiter waiter(global_registry(), extension->id()); |
| RunSetOptions(*extension, "panel_1.html", /*enabled=*/true); |
| waiter.WaitForRegistration(); |
| } |
| |
| EXPECT_TRUE(global_registry()->GetEntryForKey(extension_key)); |
| |
| { |
| // Call setOptions({enabled: false}) and wait for the extension's |
| // SidePanelEntry to be deregistered. |
| ExtensionSidePanelRegistryWaiter waiter(global_registry(), extension->id()); |
| RunSetOptions(*extension, "panel_1.html", /*enabled=*/false); |
| waiter.WaitForDeregistration(); |
| } |
| |
| EXPECT_FALSE(global_registry()->GetEntryForKey(extension_key)); |
| |
| { |
| // Sanity check that re-enabling the side panel will register the entry |
| // again and a view with the new side panel path can be shown. |
| ExtensionSidePanelRegistryWaiter waiter(global_registry(), extension->id()); |
| RunSetOptions(*extension, "panel_2.html", /*enabled=*/true); |
| waiter.WaitForRegistration(); |
| } |
| |
| EXPECT_TRUE(global_registry()->GetEntryForKey(extension_key)); |
| side_panel_coordinator()->Show(extension_key); |
| |
| // Wait until the view in the side panel is active by listening for the |
| // message sent from the view's script. |
| ASSERT_TRUE(panel_2_listener.WaitUntilSatisfied()); |
| EXPECT_TRUE(side_panel_coordinator()->IsSidePanelShowing()); |
| |
| { |
| // Calling setOptions({enabled: false}) when the extension's SidePanelEntry |
| // is shown should close the side panel. |
| ExtensionSidePanelRegistryWaiter waiter(global_registry(), extension->id()); |
| RunSetOptions(*extension, "panel_2.html", /*enabled=*/false); |
| waiter.WaitForDeregistration(); |
| } |
| |
| EXPECT_FALSE(global_registry()->GetEntryForKey(extension_key)); |
| EXPECT_FALSE(side_panel_coordinator()->IsSidePanelShowing()); |
| } |
| |
| // Test that sidePanel.setOptions() will change what is shown in the extension's |
| // SidePanelEntry's view when called with different paths. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelBrowserTest, SetOptions_Path) { |
| ExtensionTestMessageListener default_path_listener("default_path"); |
| ExtensionTestMessageListener panel_1_listener("panel_1"); |
| |
| scoped_refptr<const extensions::Extension> extension = LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/simple_default")); |
| ASSERT_TRUE(extension); |
| auto* extension_coordinator = |
| extensions::ExtensionSidePanelManager::GetOrCreateForBrowser(browser()) |
| ->GetExtensionCoordinatorForTesting(extension->id()); |
| |
| SidePanelEntry::Key extension_key = |
| SidePanelEntry::Key(SidePanelEntry::Id::kExtension, extension->id()); |
| EXPECT_TRUE(global_registry()->GetEntryForKey(extension_key)); |
| |
| // Check that the extension's side panel view shows the most recently set |
| // path. |
| RunSetOptions(*extension, "panel_1.html", /*enabled=*/true); |
| side_panel_coordinator()->Show(extension_key); |
| ASSERT_TRUE(panel_1_listener.WaitUntilSatisfied()); |
| EXPECT_FALSE(default_path_listener.was_satisfied()); |
| EXPECT_TRUE(side_panel_coordinator()->IsSidePanelShowing()); |
| |
| // Check that changing the path while the view is active will cause the view |
| // to navigate to the new path. |
| RunSetOptions(*extension, "default_path.html", /*enabled=*/true); |
| ASSERT_TRUE(default_path_listener.WaitUntilSatisfied()); |
| EXPECT_TRUE(side_panel_coordinator()->IsSidePanelShowing()); |
| |
| // Switch to the reading list in the side panel and check that the extension |
| // view is cached (i.e. the view exists but is not shown, and its web contents |
| // still exists). |
| { |
| TestSidePanelEntryWaiter reading_list_waiter( |
| global_registry()->GetEntryForKey( |
| SidePanelEntry::Key(SidePanelEntry::Id::kReadingList))); |
| side_panel_coordinator()->Show(SidePanelEntry::Id::kReadingList); |
| reading_list_waiter.WaitForEntryShown(); |
| } |
| |
| EXPECT_TRUE(global_registry()->GetEntryForKey(extension_key)->CachedView()); |
| |
| panel_1_listener.Reset(); |
| content::WebContentsDestroyedWatcher destroyed_watcher( |
| extension_coordinator->GetHostWebContentsForTesting()); |
| |
| // Test calling setOptions with a different path when the extension's view is |
| // cached. The cached view should then be invalidated and its web contents are |
| // destroyed. |
| RunSetOptions(*extension, "panel_1.html", /*enabled=*/true); |
| destroyed_watcher.Wait(); |
| |
| // When the extension's entry is shown again, the view with the updated path |
| // should be active. |
| side_panel_coordinator()->Show(extension_key); |
| ASSERT_TRUE(panel_1_listener.WaitUntilSatisfied()); |
| } |
| |
| class ExtensionSidePanelDisabledBrowserTest : public ExtensionBrowserTest { |
| public: |
| ExtensionSidePanelDisabledBrowserTest() { |
| feature_list_.InitAndDisableFeature( |
| extensions_features::kExtensionSidePanelIntegration); |
| } |
| |
| protected: |
| SidePanelRegistry* global_registry() { |
| return BrowserView::GetBrowserViewForBrowser(browser()) |
| ->side_panel_coordinator() |
| ->GetGlobalSidePanelRegistry(); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that an extension's SidePanelEntry is not registered if the |
| // `kExtensionSidePanelIntegration` feature flag is not enabled. |
| IN_PROC_BROWSER_TEST_F(ExtensionSidePanelDisabledBrowserTest, |
| NoSidePanelEntry) { |
| // Load an extension and verify that it does not have a registered |
| // SidePanelEntry as the feature is disabled. |
| scoped_refptr<const extensions::Extension> extension = LoadExtension( |
| test_data_dir_.AppendASCII("api_test/side_panel/simple_default")); |
| ASSERT_TRUE(extension); |
| SidePanelEntry::Key extension_key = |
| SidePanelEntry::Key(SidePanelEntry::Id::kExtension, extension->id()); |
| |
| EXPECT_FALSE(global_registry()->GetEntryForKey(extension_key)); |
| } |
| |
| } // namespace |
| } // namespace extensions |