910e62b5创建于 1月15日历史提交
// 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 <algorithm>

#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "components/version_info/channel.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api/offscreen/audio_lifetime_enforcer.h"
#include "extensions/browser/api/offscreen/offscreen_api.h"
#include "extensions/browser/api/offscreen/offscreen_document_manager.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/lazy_context_id.h"
#include "extensions/browser/lazy_context_task_queue.h"
#include "extensions/browser/offscreen_document_host.h"
#include "extensions/browser/script_executor.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/switches.h"
#include "extensions/test/extension_background_page_waiter.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "base/test/gtest_tags.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "content/public/common/content_client.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
#include "chrome/test/base/ui_test_utils.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

// A helper class to wait until a given WebContents is audible or inaudible.
// TODO(devlin): Put this somewhere common? //content/public/test/?
class AudioWaiter : public content::WebContentsObserver {
 public:
  explicit AudioWaiter(content::WebContents* contents)
      : content::WebContentsObserver(contents) {}

  void WaitForAudible() {
    if (web_contents()->IsCurrentlyAudible()) {
      return;
    }
    expected_state_ = true;
    run_loop_.Run();
  }

  void WaitForInaudible() {
    if (!web_contents()->IsCurrentlyAudible()) {
      return;
    }
    expected_state_ = false;
    run_loop_.Run();
  }

 private:
  void OnAudioStateChanged(bool audible) override {
    EXPECT_EQ(expected_state_, audible);
    run_loop_.QuitWhenIdle();
  }

  base::RunLoop run_loop_;
  bool expected_state_ = false;
};

// Sets the extension to be enabled in incognito mode.
scoped_refptr<const Extension> SetExtensionIncognitoEnabled(
    const Extension& extension,
    Profile& profile) {
  // Enabling the extension in incognito results in an extension reload; wait
  // for that to finish and return the new extension pointer.
  TestExtensionRegistryObserver registry_observer(
      ExtensionRegistry::Get(&profile), extension.id());
  util::SetIsIncognitoEnabled(extension.id(), &profile, true);
  scoped_refptr<const Extension> reloaded_extension =
      registry_observer.WaitForExtensionLoaded();

  if (!reloaded_extension) {
    ADD_FAILURE() << "Failed to properly reload extension.";
    return nullptr;
  }

  EXPECT_TRUE(util::IsIncognitoEnabled(reloaded_extension->id(), &profile));
  return reloaded_extension;
}

// Wakes up the service worker for the `extension` in the given `profile`.
void WakeUpServiceWorker(const Extension& extension, Profile& profile) {
  base::RunLoop run_loop;
  const auto context_id = LazyContextId::ForExtension(&profile, &extension);
  ASSERT_TRUE(context_id.IsForServiceWorker());
  context_id.GetTaskQueue()->AddPendingTask(
      context_id,
      base::BindOnce([](std::unique_ptr<LazyContextTaskQueue::ContextInfo>) {
      }).Then(run_loop.QuitWhenIdleClosure()));
  run_loop.Run();
}

}  // namespace

class OffscreenApiTest : public ExtensionApiTest {
 public:
  OffscreenApiTest() = default;
  ~OffscreenApiTest() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ExtensionApiTest::SetUpCommandLine(command_line);
    // Add the kOffscreenDocumentTesting switch to allow the use of the
    // `TESTING` reason in offscreen document creation.
    command_line->AppendSwitch(switches::kOffscreenDocumentTesting);
  }

  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(StartEmbeddedTestServer());
  }

  // Creates a new offscreen document through an API call, expecting success.
  void ProgrammaticallyCreateOffscreenDocument(const Extension& extension,
                                               Profile& profile,
                                               const char* reason = "TESTING") {
    static constexpr char kScript[] =
        R"((async () => {
             let message;
             try {
               await chrome.offscreen.createDocument(
                   {
                     url: 'offscreen.html',
                     reasons: ['%s'],
                     justification: 'testing'
                   });
               message = 'success';
             } catch (e) {
               message = 'Error: ' + e.toString();
             }
             chrome.test.sendScriptResult(message);
           })();)";
    std::string script = base::StringPrintf(kScript, reason);
    base::Value result = BackgroundScriptExecutor::ExecuteScript(
        &profile, extension.id(), script,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
    ASSERT_TRUE(result.is_string());
    EXPECT_EQ("success", result.GetString());
  }

  // Closes an offscreen document through an API call, expecting success.
  void ProgrammaticallyCloseOffscreenDocument(const Extension& extension,
                                              Profile& profile) {
    static constexpr char kScript[] =
        R"((async () => {
             let message;
             try {
               await chrome.offscreen.closeDocument();
               message = 'success';
             } catch (e) {
               message = 'Error: ' + e.toString();
             }
             chrome.test.sendScriptResult(message);
           })();)";
    base::Value result = BackgroundScriptExecutor::ExecuteScript(
        &profile, extension.id(), kScript,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
    ASSERT_TRUE(result.is_string());
    EXPECT_EQ("success", result.GetString());
  }

  // Returns the result of an API call to `runtime.getContexts()` to check if
  // an offscreen document exists. Expects the call to not throw an error,
  // independent of whether a document exists.
  bool ProgrammaticallyCheckIfHasOffscreenDocument(const Extension& extension,
                                                   Profile& profile) {
    static constexpr char kScript[] =
        R"((async () => {
             let result;
             try {
               const contexts =
                   await chrome.runtime.getContexts(
                       {contextTypes: ['OFFSCREEN_DOCUMENT']});
               if (!contexts || contexts.length > 1) {
                 throw new Error(
                     'Unexpected result: ' + JSON.stringify(contexts));
               }
               result = contexts.length == 1;
             } catch (e) {
               result = 'Error: ' + e.toString();
             }
             chrome.test.sendScriptResult(result);
           })();)";
    base::Value result = BackgroundScriptExecutor::ExecuteScript(
        &profile, extension.id(), kScript,
        BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
    EXPECT_TRUE(result.is_bool()) << result;
    return result.is_bool() && result.GetBool();
  }

 private:
  // chrome.runtime.getContexts(), used by these tests, is currently behind
  // a dev channel restriction.
  ScopedCurrentChannel current_channel_override_{version_info::Channel::DEV};
};

// Tests the general flow of creating an offscreen document.
#if BUILDFLAG(IS_MAC)
#define MAYBE_BasicDocumentManagement DISABLED_BasicDocumentManagement
#else
#define MAYBE_BasicDocumentManagement BasicDocumentManagement
#endif
IN_PROC_BROWSER_TEST_F(OffscreenApiTest, MAYBE_BasicDocumentManagement) {
  ASSERT_TRUE(RunExtensionTest("offscreen/basic_document_management"))
      << message_;
}

// Tests opening and immediately closing an offscreen document (so that the
// close happens before it's fully loaded). Regression test for
// https://crbug.com/1450784.
IN_PROC_BROWSER_TEST_F(OffscreenApiTest, OpenAndImmediatelyCloseDocument) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "background": {"service_worker": "background.js"},
           "permissions": ["offscreen"]
         })";
  static constexpr char kBackgroundJs[] =
      R"(chrome.test.runTests([
           async function openAndRapidlyClose() {
             const openResult =
                 chrome.offscreen.createDocument(
                     {
                       url: 'offscreen.html',
                       reasons: ['TESTING'],
                       justification: 'Testing'
                     });
             chrome.offscreen.closeDocument();
             await chrome.test.assertPromiseRejects(
                 openResult,
                 'Error: Offscreen document closed before fully loading.');
             chrome.test.succeed();
           },
         ]);)";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"), "<html></html>");

  ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
}

class OffscreenApiTestWithoutCommandLineFlag : public OffscreenApiTest {
 public:
  OffscreenApiTestWithoutCommandLineFlag() = default;
  ~OffscreenApiTestWithoutCommandLineFlag() override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Explicitly don't call OffscreenApiTest's version to avoid adding the
    // commandline flag.
    ExtensionApiTest::SetUpCommandLine(command_line);
  }
};

// Tests that the `TESTING` reason is disallowed without the appropriate
// commandline switch.
IN_PROC_BROWSER_TEST_F(OffscreenApiTestWithoutCommandLineFlag,
                       TestingReasonNotAllowed) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["offscreen"],
           "background": { "service_worker": "background.js" }
         })";
  static constexpr char kBackgroundJs[] =
      R"(chrome.test.runTests([
           async function cannotCreateDocumentWithTestingReason() {
             await chrome.test.assertPromiseRejects(
                 chrome.offscreen.createDocument(
                     {
                         url: 'offscreen.html',
                         reasons: ['TESTING'],
                         justification: 'testing'
                     }),
                 'Error: The `TESTING` reason is only available with the ' +
                 '--offscreen-document-testing commandline switch applied.');
             chrome.test.succeed();
           },
         ]);)";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"), "<html></html>");

  ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
}

// Tests creating, querying, and closing offscreen documents in an incognito
// spanning mode extension.
// TODO(crbug.com/40282331): Disabled on ASAN due to leak caused by renderer gin
// objects which are intended to be leaked.
// TODO(crbug.com/345326424): Flaky on Mac builds.
#if defined(ADDRESS_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_IncognitoModeHandling_SpanningMode \
  DISABLED_IncognitoModeHandling_SpanningMode
#else
#define MAYBE_IncognitoModeHandling_SpanningMode \
  IncognitoModeHandling_SpanningMode
#endif
IN_PROC_BROWSER_TEST_F(OffscreenApiTest,
                       MAYBE_IncognitoModeHandling_SpanningMode) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1",
           "background": {"service_worker": "background.js"},
           "permissions": ["offscreen"],
           "incognito": "spanning"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// Blank.");
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  extension = SetExtensionIncognitoEnabled(*extension, *profile());
  ASSERT_TRUE(extension);

  Profile* incognito_profile =
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);

  // Wake up the on-the-record service worker (the only one we have, as a
  // spanning mode extension).
  WakeUpServiceWorker(*extension, *profile());

  auto has_offscreen_document = [this, extension](Profile& profile) {
    bool programmatic =
        ProgrammaticallyCheckIfHasOffscreenDocument(*extension, profile);
    bool in_manager =
        OffscreenDocumentManager::Get(&profile)
            ->GetOffscreenDocumentForExtension(*extension) != nullptr;
    EXPECT_EQ(programmatic, in_manager) << "Mismatch between manager and API.";
    return programmatic && in_manager;
  };

  // There's less to do in a spanning mode extension - by definition, we can't
  // call any methods from an incognito profile, so we just have to verify that
  // the incognito profile is unaffected.
  ProgrammaticallyCreateOffscreenDocument(*extension, *profile());
  EXPECT_TRUE(has_offscreen_document(*profile()));
  // Don't use `has_offscreen_document()` since we can't actually check the
  // programmatic status, which requires executing script in an incognito
  // process.
  OffscreenDocumentManager* incognito_manager =
      OffscreenDocumentManager::Get(incognito_profile);
  EXPECT_EQ(nullptr,
            incognito_manager->GetOffscreenDocumentForExtension(*extension));

  ProgrammaticallyCloseOffscreenDocument(*extension, *profile());
  EXPECT_FALSE(has_offscreen_document(*profile()));
  EXPECT_EQ(nullptr,
            incognito_manager->GetOffscreenDocumentForExtension(*extension));
}

// Tests creating, querying, and closing offscreen documents in an incognito
// split mode extension.
// TODO(crbug.com/40282331): Disabled on ASAN due to leak caused by renderer gin
// objects which are intended to be leaked.
// TODO(crbug.com/345326424): Flaky on Mac builds.
#if defined(ADDRESS_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_IncognitoModeHandling_SplitMode \
  DISABLED_IncognitoModeHandling_SplitMode
#else
#define MAYBE_IncognitoModeHandling_SplitMode IncognitoModeHandling_SplitMode
#endif
IN_PROC_BROWSER_TEST_F(OffscreenApiTest,
                       MAYBE_IncognitoModeHandling_SplitMode) {
  // `split` incognito mode is required in order to allow the extension to
  // have a separate process in incognito.
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1",
           "background": {"service_worker": "background.js"},
           "permissions": ["offscreen"],
           "incognito": "split"
         })";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// Blank.");
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     "<html>offscreen</html>");

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  extension = SetExtensionIncognitoEnabled(*extension, *profile());
  ASSERT_TRUE(extension);

  Profile* incognito_profile =
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);

  // We're going to be executing scripts in the service worker context, so
  // ensure the service worker is active.
  // TODO(devlin): Should BackgroundScriptExecutor handle that for us? (Perhaps
  // optionally?)
  WakeUpServiceWorker(*extension, *profile());
  WakeUpServiceWorker(*extension, *incognito_profile);

  auto has_offscreen_document = [this, extension](Profile& profile) {
    bool programmatic =
        ProgrammaticallyCheckIfHasOffscreenDocument(*extension, profile);
    bool in_manager =
        OffscreenDocumentManager::Get(&profile)
            ->GetOffscreenDocumentForExtension(*extension) != nullptr;
    EXPECT_EQ(programmatic, in_manager) << "Mismatch between manager and API.";
    return programmatic && in_manager;
  };

  // Create an offscreen document in the on-the-record profile. Only it should
  // have a document; the off-the-record profile is considered distinct.
  ProgrammaticallyCreateOffscreenDocument(*extension, *profile());
  EXPECT_TRUE(has_offscreen_document(*profile()));
  EXPECT_FALSE(has_offscreen_document(*incognito_profile));

  // Now, create a new document in the off-the-record profile.
  ProgrammaticallyCreateOffscreenDocument(*extension, *incognito_profile);
  EXPECT_TRUE(has_offscreen_document(*profile()));
  EXPECT_TRUE(has_offscreen_document(*incognito_profile));

  // Close the off-the-record profile - the on-the-record profile's offscreen
  // document should remain open.
  ProgrammaticallyCloseOffscreenDocument(*extension, *incognito_profile);
  EXPECT_TRUE(has_offscreen_document(*profile()));
  EXPECT_FALSE(has_offscreen_document(*incognito_profile));

  // Finally, close the on-the-record profile's document.
  ProgrammaticallyCloseOffscreenDocument(*extension, *profile());
  EXPECT_FALSE(has_offscreen_document(*profile()));
  EXPECT_FALSE(has_offscreen_document(*incognito_profile));
}

IN_PROC_BROWSER_TEST_F(OffscreenApiTest, LifetimeEnforcement) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1",
           "background": {"service_worker": "background.js"},
           "permissions": ["offscreen"]
         })";
  // An offscreen document that knows how to play audio.
  static constexpr char kOffscreenJs[] =
      R"(function playAudio() {
           const audioTag = document.createElement('audio');
           audioTag.src = '_test_resources/long_audio.ogg';
           document.body.appendChild(audioTag);
           audioTag.play();
         }

         function stopAudio() {
           document.body.getElementsByTagName('audio')[0].pause();
         }

         chrome.runtime.onMessage.addListener((msg) => {
           if (msg == 'play')
             playAudio();
           else if (msg == 'stop')
             stopAudio();
           else
             console.error('Unexpected message: ' + msg);
         }))";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// Blank.");
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"),
                     R"(<html><script src="offscreen.js"></script></html>)");
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.js"), kOffscreenJs);

  scoped_refptr<const Extension> extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Create a new offscreen document for audio playback and wait for it to load.
  OffscreenDocumentManager* manager = OffscreenDocumentManager::Get(profile());
  ProgrammaticallyCreateOffscreenDocument(*extension, *profile(),
                                          "AUDIO_PLAYBACK");
  OffscreenDocumentHost* document =
      manager->GetOffscreenDocumentForExtension(*extension);
  ASSERT_TRUE(document);
  content::WaitForLoadStop(document->host_contents());

  // Begin the audio playback.
  {
    AudioWaiter audio_waiter(document->host_contents());
    BackgroundScriptExecutor::ExecuteScriptAsync(
        profile(), extension->id(), "chrome.runtime.sendMessage('play');");
    audio_waiter.WaitForAudible();
  }

  // The document should be kept alive.
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(manager->GetOffscreenDocumentForExtension(*extension));

  // Override the timeout. We can't do this at the top of the test because
  // otherwise, the document would immediately be considered inactive.
  auto timeout_override =
      AudioLifetimeEnforcer::SetTimeoutForTesting(base::Seconds(0));

  // Now, stop the audio.
  {
    AudioWaiter audio_waiter(document->host_contents());
    BackgroundScriptExecutor::ExecuteScriptAsync(
        profile(), extension->id(), "chrome.runtime.sendMessage('stop');");
    audio_waiter.WaitForInaudible();
  }

  // The offscreen document should be closed since it is no longer active.
  // `document` is now unsafe to use.
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(manager->GetOffscreenDocumentForExtension(*extension));
}

// Tests opening an offscreen document that takes awhile to load properly waits
// for the document to load before resolving the promise, ensuring the document
// is ready to receive messages by the time the promise resolves.
IN_PROC_BROWSER_TEST_F(OffscreenApiTest, LongLoadOffscreenDocument) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["offscreen"],
           "background": { "service_worker": "background.js" }
         })";
  static constexpr char kOffscreenHtml[] =
      R"(<html><script src="offscreen.js"></script></html>)";
  // This script busy-waits for two seconds before (synchronously) adding a
  // message listener.
  static constexpr char kOffscreenJs[] =
      R"(const startTime = performance.now();
         const endTime = startTime + 2000;
         while (performance.now() < endTime) { /* Spin our wheels! */ }
         chrome.runtime.onMessage.addListener((msg, sender, reply) => {
           reply(msg + ' reply');
         });)";
  // The background script will open an offscreen document and, once the
  // createDocument() call resolves, send a message. Since createDocument()
  // should wait for document to finish loading, this should work.
  static constexpr char kBackgroundJs[] =
      R"(chrome.test.runTests([
           async function longLoadDocAndSendMessage() {
             await chrome.offscreen.createDocument(
                       {
                           url: 'offscreen.html',
                           reasons: ['TESTING'],
                           justification: 'testing'
                       });
             const reply = await chrome.runtime.sendMessage('test message');
             chrome.test.assertEq('test message reply', reply);
             chrome.test.succeed();
           },
         ]);)";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"), kOffscreenHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.js"), kOffscreenJs);

  ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
}

// TODO(crbug.com/378916068): Enable the following tests on desktop Android
// when chrome.action is supported on desktop Android.
#if BUILDFLAG(ENABLE_EXTENSIONS)
// TODO(crbug.com/40272130): Failing on Windows.
#if BUILDFLAG(IS_WIN)
#define MAYBE_TabCaptureStreams DISABLED_TabCaptureStreams
#else
#define MAYBE_TabCaptureStreams TabCaptureStreams
#endif
IN_PROC_BROWSER_TEST_F(OffscreenApiTest, MAYBE_TabCaptureStreams) {
  const Extension* extension = LoadExtension(
      test_data_dir_.AppendASCII("offscreen/tab_capture_streams"));
  ASSERT_TRUE(extension);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(),
      embedded_test_server()->GetURL("example.com", "/simple.html")));

  // Tab capture requires active tab, so click on the action to grant permission
  // and kick off the tests.
  ResultCatcher result_catcher;
  ExtensionActionTestHelper::Create(browser())->Press(extension->id());
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

// Tests user gestures are curried from service workers into offscreen
// documents.
IN_PROC_BROWSER_TEST_F(OffscreenApiTest,
                       UserGesturesAreCurriedFromServiceWorkers) {
  static constexpr char kManifest[] =
      R"({
           "name": "Offscreen Document Test",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["offscreen"],
           "action": {},
           "background": { "service_worker": "background.js" }
         })";
  static constexpr char kOffscreenHtml[] =
      R"(<html><script src="offscreen.js"></script></html>)";
  static constexpr char kOffscreenJs[] =
      R"(chrome.runtime.onMessage.addListener((msg, sender, sendReply) => {
           try {
             const activeGesture = chrome.test.isProcessingUserGesture();
             sendReply('active gesture: ' + activeGesture);
           } catch (e) {
             sendReply(`Error: ${e.toString()}`);
           }
         });)";
  // The extension background script will:
  // - Open a new offscreen document
  // - Wait for an action click. This includes an active user action.
  // - In the listener for the action click, dispatch a message to the
  //   offscreen document. The active user gesture should be curried along.
  static constexpr char kBackgroundJs[] =
      R"((async () => {
             await chrome.offscreen.createDocument(
                       {
                           url: 'offscreen.html',
                           reasons: ['TESTING'],
                           justification: 'testing'
                       });
             chrome.test.sendMessage('opened');
         })();
         chrome.action.onClicked.addListener(() => {
           chrome.test.assertTrue(chrome.test.isProcessingUserGesture());
           chrome.runtime.sendMessage('test message').then(response => {
             chrome.test.assertEq('active gesture: true', response);
             chrome.test.succeed();
           });
         });)";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.html"), kOffscreenHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("offscreen.js"), kOffscreenJs);

  ExtensionTestMessageListener test_listener("opened");
  ResultCatcher result_catcher;
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  ASSERT_TRUE(test_listener.WaitUntilSatisfied());
  ExtensionActionTestHelper::Create(browser())->Press(extension->id());
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
#endif

}  // namespace extensions