| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_BROWSERTEST_H_ |
| #define CHROME_BROWSER_EXTENSIONS_EXTENSION_BROWSERTEST_H_ |
| |
| #include "base/files/file_path.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/scoped_observation.h" |
| #include "base/test/scoped_path_override.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/extensions/extension_browser_test_util.h" |
| #include "chrome/browser/extensions/extension_browsertest_platform_delegate.h" |
| #include "chrome/browser/extensions/install_verifier.h" |
| #include "chrome/browser/extensions/updater/extension_updater.h" |
| #include "chrome/test/base/platform_browser_test.h" |
| #include "extensions/browser/browsertest_util.h" |
| #include "extensions/browser/disable_reason.h" |
| #include "extensions/browser/extension_creator.h" |
| #include "extensions/browser/extension_protocols.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_registry_observer.h" |
| #include "extensions/browser/sandboxed_unpacker.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/feature_switch.h" |
| #include "extensions/common/features/feature_channel.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "chrome/browser/extensions/scoped_test_mv2_enabler.h" |
| #endif |
| |
| class Profile; |
| |
| namespace content { |
| class RenderFrameHost; |
| class ServiceWorkerContext; |
| class WebContents; |
| } // namespace content |
| |
| namespace extensions { |
| class Extension; |
| class ExtensionCache; |
| class ExtensionHost; |
| class ExtensionRegistrar; |
| class ExtensionSet; |
| class ExtensionTestNotificationObserver; |
| class ProcessManager; |
| class ScopedIgnoreContentVerifierForTest; |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| class ExtensionService; |
| #endif |
| |
| // A cross-platform base class for extensions-related browser tests. |
| // `PlatformBrowserTest` inherits from different test suites based on the |
| // platform; `ExtensionBrowserTest` provides additional functionality |
| // that is available on all platforms. |
| class ExtensionBrowserTest : public PlatformBrowserTest, |
| public ExtensionRegistryObserver { |
| public: |
| using LoadOptions = extensions::browser_test_util::LoadOptions; |
| using ContextType = extensions::browser_test_util::ContextType; |
| |
| explicit ExtensionBrowserTest(ContextType context_type = ContextType::kNone); |
| ExtensionBrowserTest(const ExtensionBrowserTest&) = delete; |
| ExtensionBrowserTest& operator=(const ExtensionBrowserTest&) = delete; |
| ~ExtensionBrowserTest() override; |
| |
| protected: |
| // Specifies the type of UI (if any) to show during installation and what |
| // user action to simulate. |
| enum class InstallUIType { |
| kNone, |
| kCancel, |
| kNormal, |
| kAutoConfirm, |
| }; |
| |
| // The platform delegate is an implementation detail of the test harness |
| // and should be able to access anything any general test would access. |
| friend class ExtensionBrowserTestPlatformDelegate; |
| |
| // Extensions used in tests are typically not from the web store and will have |
| // missing content verification hashes. The default implementation disables |
| // content verification; this should be overridden by derived tests which care |
| // about content verification. |
| virtual bool ShouldEnableContentVerification(); |
| |
| // Extensions used in tests are typically not from the web store and will fail |
| // install verification. The default implementation disables install |
| // verification; this should be overridden by derived tests which care |
| // about install verification. |
| virtual bool ShouldEnableInstallVerification(); |
| |
| // Whether MV2 extensions should be allowed. Defaults to true for testing |
| // (since many tests are parameterized to exercise both MV2 + MV3 logic). |
| virtual bool ShouldAllowMV2Extensions(); |
| |
| // Returns the extension in `extensions` with the given `path`, if one exists. |
| static const Extension* GetExtensionByPath(const ExtensionSet& extensions, |
| const base::FilePath& path); |
| |
| // content::BrowserTestBase: |
| void SetUp() override; |
| void SetUpCommandLine(base::CommandLine* command_line) override; |
| void SetUpOnMainThread() override; |
| void TearDown() override; |
| void TearDownOnMainThread() override; |
| |
| // ExtensionRegistryObserver: |
| void OnExtensionLoaded(content::BrowserContext* browser_context, |
| const Extension* extension) override; |
| void OnShutdown(ExtensionRegistry* registry) override; |
| |
| // Lower-case to match ExtensionBrowserTest. |
| ExtensionRegistry* extension_registry(); |
| ExtensionRegistrar* extension_registrar(); |
| |
| // Returns the path of the directory from which to serve resources when they |
| // are prefixed with "_test_resources/". |
| // The default is chrome/test/data/extensions/. |
| virtual base::FilePath GetTestResourcesParentDir(); |
| |
| const Extension* LoadExtension(const base::FilePath& path); |
| const Extension* LoadExtension(const base::FilePath& path, |
| const LoadOptions& options); |
| |
| // Loads unpacked extension from `path` with manifest `manifest_relative_path` |
| // and imitates that it is a component extension. |
| // `manifest_relative_path` is relative to `path`. |
| const Extension* LoadExtensionAsComponentWithManifest( |
| const base::FilePath& path, |
| const base::FilePath::CharType* manifest_relative_path); |
| |
| // Loads unpacked extension from `path` and imitates that it is a component |
| // extension. Equivalent to |
| // `LoadExtensionAsComponentWithManifest(path, kManifestFilename)`. |
| const Extension* LoadExtensionAsComponent(const base::FilePath& path); |
| |
| // `expected_change` indicates how many extensions should be installed (or |
| // disabled, if negative). |
| // 1 means you expect a new install, 0 means you expect an upgrade, -1 means |
| // you expect a failed upgrade. |
| const Extension* InstallExtension(const base::FilePath& path, |
| std::optional<int> expected_change); |
| |
| // Same as above, but an install source other than |
| // mojom::ManifestLocation::kInternal can be specified. |
| const Extension* InstallExtension(const base::FilePath& path, |
| std::optional<int> expected_change, |
| mojom::ManifestLocation install_source); |
| |
| // Installs an extension and grants it the permissions it requests. |
| // TODO(devlin): It seems like this is probably the desired outcome most of |
| // the time - otherwise the extension installs in a disabled state. |
| const Extension* InstallExtensionWithPermissionsGranted( |
| const base::FilePath& file_path, |
| std::optional<int> expected_change); |
| |
| // Installs extension as if it came from the Chrome Webstore. |
| const Extension* InstallExtensionFromWebstore( |
| const base::FilePath& path, |
| std::optional<int> expected_change); |
| |
| const Extension* InstallExtensionWithUIAutoConfirm( |
| const base::FilePath& path, |
| std::optional<int> expected_change); |
| |
| const Extension* InstallExtensionWithSourceAndFlags( |
| const base::FilePath& path, |
| std::optional<int> expected_change, |
| mojom::ManifestLocation install_source, |
| Extension::InitFromValueFlags creation_flags); |
| |
| // Begins install process but simulates a user cancel. |
| const Extension* StartInstallButCancel(const base::FilePath& path); |
| |
| // Same as above but passes an id to CrxInstaller and does not allow a |
| // privilege increase. |
| const Extension* UpdateExtension(const extensions::ExtensionId& id, |
| const base::FilePath& path, |
| std::optional<int> expected_change); |
| |
| // Same as UpdateExtension but waits for the extension to be idle first. |
| const Extension* UpdateExtensionWaitForIdle( |
| const extensions::ExtensionId& id, |
| const base::FilePath& path, |
| std::optional<int> expected_change); |
| |
| void DisableExtension(const ExtensionId& extension_id); |
| void DisableExtension(const ExtensionId& extension_id, |
| const DisableReasonSet& disable_reasons); |
| |
| // Unloads the extension with the given `extension_id`. |
| void UnloadExtension(const ExtensionId& extension_id); |
| |
| // Uninstalls the extension with the given `extension_id`. |
| void UninstallExtension(const ExtensionId& extension_id); |
| |
| // Enables the extension with the given `extension_id`. |
| void EnableExtension(const ExtensionId& extension_id); |
| |
| // Reloads the extension with the given `extension_id`. |
| void ReloadExtension(const ExtensionId& extension_id); |
| |
| // Returns the WebContents of the currently active tab. |
| // Note that when the test first launches, this will be the same as the |
| // default tab's web_contents(). However, if the test creates new tabs and |
| // switches the active tab, this will return the WebContents of the new active |
| // tab. |
| content::WebContents* GetActiveWebContents() const; |
| |
| // Returns incognito profile. Creates the profile if it doesn't exist. |
| Profile* GetOrCreateIncognitoProfile(); |
| |
| // Pack the extension in `dir_path` into a crx file and return its path. |
| // Return an empty FilePath if there were errors. |
| base::FilePath PackExtension( |
| const base::FilePath& dir_path, |
| int extra_run_flags = ExtensionCreator::kNoRunFlags); |
| |
| // Pack the extension in `dir_path` into a crx file at `crx_path`, using the |
| // key `pem_path`. If `pem_path` does not exist, create a new key at |
| // `pem_out_path`. |
| // Return the path to the crx file, or an empty FilePath if there were errors. |
| base::FilePath PackExtensionWithOptions( |
| const base::FilePath& dir_path, |
| const base::FilePath& crx_path, |
| const base::FilePath& pem_path, |
| const base::FilePath& pem_out_path, |
| int extra_run_flags = ExtensionCreator::kNoRunFlags); |
| |
| // Navigates to a `url` in the active web contents and waits until the |
| // navigation finishes. Returns true on success. |
| [[nodiscard]] bool NavigateToURL(const GURL& url); |
| |
| // Opens `url` in an incognito browser window with the incognito profile of |
| // `profile`, blocking until the navigation finishes. Returns the WebContents |
| // for `url`. |
| content::WebContents* PlatformOpenURLOffTheRecord(Profile* profile, |
| const GURL& url); |
| |
| // Opens `url` in a new tab, blocking until the navigation finishes. |
| content::RenderFrameHost* NavigateToURLInNewTab(const GURL& url); |
| |
| // Simulates a page calling window.open on an URL and waits for the |
| // navigation. |
| // `should_succeed` indicates whether the navigation should succeed, in which |
| // case the last committed url should match the passed url and the page should |
| // not be an error or interstitial page. |
| void OpenWindow(content::WebContents* contents, |
| const GURL& url, |
| bool newtab_process_should_equal_opener, |
| bool should_succeed, |
| content::WebContents** newtab_result); |
| |
| // Simulates a page navigating itself to an URL and waits for the |
| // navigation. Returns true if the navigation succeeds. |
| [[nodiscard]] bool NavigateInRenderer(content::WebContents* contents, |
| const GURL& url); |
| |
| // Looks for an ExtensionHost whose URL has the given path component |
| // (including leading slash). Also verifies that the expected number of hosts |
| // are loaded. |
| ExtensionHost* FindHostWithPath(ProcessManager* manager, |
| const std::string& path, |
| int expected_hosts); |
| |
| // Get the ServiceWorkerContext for the default browser's profile. |
| content::ServiceWorkerContext* GetServiceWorkerContext(); |
| |
| // Get the ServiceWorkerContext for the `browser_context`. |
| static content::ServiceWorkerContext* GetServiceWorkerContext( |
| content::BrowserContext* browser_context); |
| |
| // Returns the number of tabs in the current window. |
| int GetTabCount(); |
| |
| // Returns whether the tab at `index` is selected. |
| bool IsTabSelected(int index); |
| |
| // Closes the tab associated with `web_contents`. |
| void CloseTabForWebContents(content::WebContents* web_contents); |
| |
| // Waits until `script` calls "chrome.test.sendScriptResult(result)", |
| // where `result` is a serializable value, and returns `result`. Fails |
| // the test and returns an empty base::Value if `extension_id` isn't |
| // installed in the test's profile or doesn't have a background page, or |
| // if executing the script fails. The argument `script_user_activation` |
| // determines if the script should be executed after a user activation. |
| base::Value ExecuteScriptInBackgroundPage( |
| const extensions::ExtensionId& extension_id, |
| const std::string& script, |
| browsertest_util::ScriptUserActivation script_user_activation = |
| browsertest_util::ScriptUserActivation::kDontActivate); |
| |
| // Waits until `script` calls "window.domAutomationController.send(result)", |
| // where `result` is a string, and returns `result`. Fails the test and |
| // returns an empty base::Value if `extension_id` isn't installed in test's |
| // profile or doesn't have a background page, or if executing the script |
| // fails. The argument `script_user_activation` determines if the script |
| // should be executed after a user activation. |
| std::string ExecuteScriptInBackgroundPageDeprecated( |
| const extensions::ExtensionId& extension_id, |
| const std::string& script, |
| browsertest_util::ScriptUserActivation script_user_activation = |
| browsertest_util::ScriptUserActivation::kDontActivate); |
| |
| bool ExecuteScriptInBackgroundPageNoWait( |
| const extensions::ExtensionId& extension_id, |
| const std::string& script, |
| browsertest_util::ScriptUserActivation script_user_activation = |
| browsertest_util::ScriptUserActivation::kDontActivate); |
| |
| // Sets up `test_protocol_handler_` so that |
| // chrome-extensions://<extension_id>/_test_resources/foo maps to |
| // chrome/test/data/extensions/foo. |
| void SetUpTestProtocolHandler(); |
| |
| // Tears down test protocol handler. |
| void TearDownTestProtocolHandler(); |
| |
| // Wait for all extension views to load. |
| bool WaitForExtensionViewsToLoad(); |
| |
| // Wait for the extension to be idle. |
| bool WaitForExtensionIdle(const ExtensionId& extension_id); |
| |
| // Wait for the extension to not be idle. |
| bool WaitForExtensionNotIdle(const ExtensionId& extension_id); |
| |
| // These match the methods in ExtensionBrowserTestPlatformDelegate: |
| const Extension* LoadAndLaunchApp(const base::FilePath& path, |
| bool uses_guest_view = false); |
| bool WaitForPageActionVisibilityChangeTo(int count); |
| |
| // Lower case to match the style of InProcessBrowserTest. |
| virtual Profile* profile(); |
| |
| // WebContents* of the default tab or nullptr if the default tab is destroyed. |
| content::WebContents* web_contents(); |
| |
| const ExtensionId& last_loaded_extension_id() { |
| return last_loaded_extension_id_; |
| } |
| void set_last_loaded_extension_id(ExtensionId id) { |
| last_loaded_extension_id_ = std::move(id); |
| } |
| |
| ExtensionTestNotificationObserver* test_notification_observer() { |
| return test_notification_observer_.get(); |
| } |
| |
| ExtensionBrowserTestPlatformDelegate& platform_delegate() { |
| return platform_delegate_; |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Note: ExtensionService is not available in desktop android builds. |
| ExtensionService* extension_service(); |
| #endif |
| |
| // Set to "chrome/test/data/extensions". Derived classes may override. |
| base::FilePath test_data_dir_; |
| |
| const ContextType context_type_; |
| |
| // An override so that chrome-extensions://<extension_id>/_test_resources/foo |
| // maps to chrome/test/data/extensions/foo. |
| ExtensionProtocolTestHandler test_protocol_handler_; |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // True if the command line should be tweaked as if ChromeOS user is |
| // already logged in. |
| bool set_chromeos_user_ = true; |
| #endif |
| |
| private: |
| // Common implementation for all our various install and update methods. |
| const Extension* InstallOrUpdateExtension( |
| const extensions::ExtensionId& id, |
| const base::FilePath& path, |
| InstallUIType ui_type, |
| std::optional<int> expected_change, |
| mojom::ManifestLocation install_source, |
| content::WebContents* active_web_contents, |
| Extension::InitFromValueFlags creation_flags, |
| bool wait_for_idle, |
| bool grant_permissions); |
| |
| ExtensionBrowserTestPlatformDelegate platform_delegate_; |
| |
| // Temporary directory for testing. |
| base::ScopedTempDir temp_dir_; |
| |
| // WebContents* of the default tab or nullptr if the default tab is destroyed. |
| base::WeakPtr<content::WebContents> web_contents_; |
| |
| ExtensionId last_loaded_extension_id_; |
| |
| #if BUILDFLAG(ENABLE_DESKTOP_ANDROID_EXTENSIONS) |
| class TestTabModel; |
| std::unique_ptr<TestTabModel> tab_model_; |
| #endif |
| |
| // Used for setting the default scoped current channel for extension browser |
| // tests to UNKNOWN (trunk), in order to enable channel restricted features. |
| // TODO(crbug.com/40261741): We should remove this and have the current |
| // channel respect what is defined on the builder. If a test requires a |
| // specific channel for a channel restricted feature, it should be defining |
| // its own scoped channel override. As this stands, it means we don't really |
| // have non-trunk coverage for most extension browser tests. |
| ScopedCurrentChannel current_channel_; |
| |
| // Disable external install UI. |
| FeatureSwitch::ScopedOverride override_prompt_for_external_extensions_; |
| |
| #if BUILDFLAG(IS_WIN) |
| // Use mock shortcut directories to ensure app shortcuts are cleaned up. |
| base::ScopedPathOverride user_desktop_override_; |
| base::ScopedPathOverride common_desktop_override_; |
| base::ScopedPathOverride user_quick_launch_override_; |
| base::ScopedPathOverride start_menu_override_; |
| base::ScopedPathOverride common_start_menu_override_; |
| #endif |
| |
| std::unique_ptr<ExtensionCache> test_extension_cache_; |
| |
| // Conditionally disable install verification. |
| std::unique_ptr<ScopedInstallVerifierBypassForTest> |
| ignore_install_verification_; |
| |
| // Conditionally disable content verification. |
| std::unique_ptr<ScopedIgnoreContentVerifierForTest> |
| ignore_content_verification_; |
| |
| // Used to disable CRX publisher signature checking. |
| SandboxedUnpacker::ScopedVerifierFormatOverrideForTest |
| verifier_format_override_; |
| |
| ExtensionUpdater::ScopedSkipScheduledCheckForTest skip_scheduled_check_; |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // Allows MV2 extensions to be loaded. |
| std::optional<ScopedTestMV2Enabler> mv2_enabler_; |
| #endif |
| |
| std::unique_ptr<ExtensionTestNotificationObserver> |
| test_notification_observer_; |
| |
| // Listens to extension loaded notifications. |
| base::ScopedObservation<ExtensionRegistry, ExtensionRegistryObserver> |
| registry_observation_{this}; |
| }; |
| |
| } // namespace extensions |
| |
| #endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_BROWSERTEST_H_ |