// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Contains holistic tests of the bindings infrastructure

#include "base/run_loop.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/api/permissions/permissions_api.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "components/embedder_support/switches.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/process_manager.h"
#include "extensions/buildflags/buildflags.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 "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "third_party/blink/public/common/switches.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {
namespace {

void MouseDownInWebContents(content::WebContents* web_contents) {
  blink::WebMouseEvent mouse_event(
      blink::WebInputEvent::Type::kMouseDown,
      blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  mouse_event.button = blink::WebMouseEvent::Button::kLeft;
  mouse_event.SetPositionInWidget(10, 10);
  mouse_event.click_count = 1;
  web_contents->GetPrimaryMainFrame()
      ->GetRenderViewHost()
      ->GetWidget()
      ->ForwardMouseEvent(mouse_event);
}

void MouseUpInWebContents(content::WebContents* web_contents) {
  blink::WebMouseEvent mouse_event(
      blink::WebInputEvent::Type::kMouseUp, blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  mouse_event.button = blink::WebMouseEvent::Button::kLeft;
  mouse_event.SetPositionInWidget(10, 10);
  mouse_event.click_count = 1;
  web_contents->GetPrimaryMainFrame()
      ->GetRenderViewHost()
      ->GetWidget()
      ->ForwardMouseEvent(mouse_event);
}

class ExtensionBindingsApiTest : public ExtensionApiTest {
 public:
  ExtensionBindingsApiTest() = default;

  ExtensionBindingsApiTest(const ExtensionBindingsApiTest&) = delete;
  ExtensionBindingsApiTest& operator=(const ExtensionBindingsApiTest&) = delete;

  ~ExtensionBindingsApiTest() override = default;

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

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ExtensionApiTest::SetUpCommandLine(command_line);
    // Some bots are flaky due to slower loading interacting with
    // deferred commits.
    command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
  }
};

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       UnavailableBindingsNeverRegistered) {
  // Test will request the 'storage' permission.
  PermissionsRequestFunction::SetIgnoreUserGestureForTests(true);
  ASSERT_TRUE(RunExtensionTest(
      "bindings/unavailable_bindings_never_registered")) << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       ExceptionInHandlerShouldNotCrash) {
  ASSERT_TRUE(RunExtensionTest("bindings/exception_in_handler_should_not_crash",
                               {.extension_url = "page.html"}))
      << message_;
}

// Tests that an error raised during an async function still fires
// the callback, but sets chrome.runtime.lastError.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, LastError) {
  ExtensionTestMessageListener ready_listener("ready");
  ASSERT_TRUE(LoadExtension(
      test_data_dir_.AppendASCII("bindings").AppendASCII("last_error")));
  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());

  // Get the ExtensionHost that is hosting our background page.
  extensions::ProcessManager* manager =
      extensions::ProcessManager::Get(profile());
  extensions::ExtensionHost* host = FindHostWithPath(manager, "/bg.html", 1);
  ASSERT_TRUE(host);

  EXPECT_EQ(true, content::EvalJs(host->host_contents(), "testLastError()"));
}

// Regression test that we don't delete our own bindings with about:blank
// iframes.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, AboutBlankIframe) {
  ResultCatcher catcher;
  ExtensionTestMessageListener listener("load", ReplyBehavior::kWillReply);

  ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("bindings")
                                          .AppendASCII("about_blank_iframe")));

  ASSERT_TRUE(listener.WaitUntilSatisfied());

  const Extension* extension = LoadExtension(
        test_data_dir_.AppendASCII("bindings")
                      .AppendASCII("internal_apis_not_on_chrome_object"));
  ASSERT_TRUE(extension);
  listener.Reply(extension->id());

  ASSERT_TRUE(catcher.GetNextResult()) << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       InternalAPIsNotOnChromeObject) {
  ASSERT_TRUE(RunExtensionTest("bindings/internal_apis_not_on_chrome_object",
                               {.extension_url = "page.html"}))
      << message_;
}

// Tests that we don't override events when bindings are re-injected.
// Regression test for http://crbug.com/40327962.
// Regression test for http://crbug.com/40395582.
// Flaky http://crbug.com/41325266.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, DISABLED_EventOverriding) {
  ASSERT_TRUE(RunExtensionTest("bindings/event_overriding")) << message_;
  // The extension test removes a window and, during window removal, sends the
  // success message. Make sure we flush all pending tasks.
  base::RunLoop().RunUntilIdle();
}

// Tests the effectiveness of the 'nocompile' feature file property.
// Regression test for http://crbug.com/41097076.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, Nocompile) {
  ASSERT_TRUE(
      RunExtensionTest("bindings/nocompile", {.extension_url = "page.html"}))
      << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, ApiEnums) {
  ASSERT_TRUE(RunExtensionTest("bindings/api_enums")) << message_;
}

// Regression test for http://crbug.com/40082354 - proper access checks on
// getModuleSystem().
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, ModuleSystem) {
  ASSERT_TRUE(RunExtensionTest("bindings/module_system")) << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, NoExportOverriding) {
  // We need to create runtime bindings in the web page. An extension that's
  // externally connectable will do that for us.
  ASSERT_TRUE(LoadExtension(
      test_data_dir_.AppendASCII("bindings")
                    .AppendASCII("externally_connectable_everywhere")));

  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL(
          "/extensions/api_test/bindings/override_exports.html")));

  // See chrome/test/data/extensions/api_test/bindings/override_exports.html.
  EXPECT_EQ(
      "success",
      content::EvalJs(GetActiveWebContents(),
                      "document.getElementById('status').textContent.trim();"));
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, NoGinDefineOverriding) {
  // We need to create runtime bindings in the web page. An extension that's
  // externally connectable will do that for us.
  ASSERT_TRUE(LoadExtension(
      test_data_dir_.AppendASCII("bindings")
                    .AppendASCII("externally_connectable_everywhere")));

  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL(
          "/extensions/api_test/bindings/override_gin_define.html")));
  ASSERT_FALSE(web_contents->IsCrashed());

  // See chrome/test/data/extensions/api_test/bindings/override_gin_define.html.
  EXPECT_EQ(
      "success",
      content::EvalJs(web_contents,
                      "document.getElementById('status').textContent.trim();"));
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, HandlerFunctionTypeChecking) {
  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL("/extensions/api_test/bindings/"
                                     "handler_function_type_checking.html")));
  EXPECT_FALSE(web_contents->IsCrashed());
  // See handler_function_type_checking.html.
  EXPECT_EQ(
      "success",
      content::EvalJs(web_contents,
                      "document.getElementById('status').textContent.trim();"));
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       MoreNativeFunctionInterceptionTests) {
  // We need to create runtime bindings in the web page. An extension that's
  // externally connectable will do that for us.
  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("bindings")
                        .AppendASCII("externally_connectable_everywhere")));

  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL(
          "/extensions/api_test/bindings/function_interceptions.html")));
  EXPECT_FALSE(web_contents->IsCrashed());
  // See function_interceptions.html.
  EXPECT_EQ("success", content::EvalJs(web_contents, "window.testStatus;"));
}

class FramesExtensionBindingsApiTest : public ExtensionBindingsApiTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    ExtensionBindingsApiTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(embedder_support::kDisablePopupBlocking);
  }
};

// This tests that web pages with iframes or child windows pointing at
// chrome-extension:// urls, both web_accessible and nonexistent pages, don't
// get improper extensions bindings injected while they briefly still point at
// about:blank and are still scriptable by their parent.
//
// The general idea is to load up 2 extensions, one which listens for external
// messages ("receiver") and one which we'll try first faking messages from in
// the web page's iframe, as well as actually send a message from later
// ("sender").
IN_PROC_BROWSER_TEST_F(FramesExtensionBindingsApiTest, FramesBeforeNavigation) {
  // Load the sender and receiver extensions, and make sure they are ready.
  ExtensionTestMessageListener sender_ready("sender_ready",
                                            ReplyBehavior::kWillReply);
  const Extension* sender = LoadExtension(
      test_data_dir_.AppendASCII("bindings").AppendASCII("message_sender"));
  ASSERT_NE(nullptr, sender);
  ASSERT_TRUE(sender_ready.WaitUntilSatisfied());

  ExtensionTestMessageListener receiver_ready("receiver_ready");
  const Extension* receiver =
      LoadExtension(test_data_dir_.AppendASCII("bindings")
                        .AppendASCII("external_message_listener"));
  ASSERT_NE(nullptr, receiver);
  ASSERT_TRUE(receiver_ready.WaitUntilSatisfied());

  // Load the web page which tries to impersonate the sender extension via
  // scripting iframes/child windows before they finish navigating to pages
  // within the sender extension.
  auto* web_contents = GetActiveWebContents();
  // This doesn't reliably return true/false on all bots, so ignore the result.
  (void)NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL(
          "/extensions/api_test/bindings/frames_before_navigation.html"));

  EXPECT_EQ(true, content::EvalJs(web_contents, "getResult()"));

  // Reply to |sender|, causing it to send a message over to |receiver|, and
  // then ask |receiver| for the total message count. It should be 1 since
  // |receiver| should not have received any impersonated messages.
  sender_ready.Reply(receiver->id());
  EXPECT_EQ(
      1, content::EvalJs(ProcessManager::Get(profile())
                             ->GetBackgroundHostForExtension(receiver->id())
                             ->host_contents(),
                         "getMessageCountAfterReceivingRealSenderMessage()"));
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, TestFreezingChrome) {
  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents,
                            embedded_test_server()->GetURL(
                                "/extensions/api_test/bindings/freeze.html")));
  ASSERT_FALSE(web_contents->IsCrashed());
}

// Tests interaction with event filter parsing.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, TestEventFilterParsing) {
  ExtensionTestMessageListener listener("ready");
  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("bindings/event_filter")));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  ResultCatcher catcher;
  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("example.com", "/title1.html")));
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// crbug.com/40525791
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, ValidationInterception) {
  // We need to create runtime bindings in the web page. An extension that's
  // externally connectable will do that for us.
  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("bindings")
                        .AppendASCII("externally_connectable_everywhere")));

  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL(
          "/extensions/api_test/bindings/validation_interception.html")));
  EXPECT_TRUE(content::WaitForLoadStop(web_contents));
  ASSERT_FALSE(web_contents->IsCrashed());
  EXPECT_EQ(true, content::EvalJs(web_contents, "caught"));
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, UncaughtExceptionLogging) {
  ASSERT_TRUE(RunExtensionTest("bindings/uncaught_exception_logging"))
      << message_;
}

// Verify that when a web frame embeds an extension subframe, and that subframe
// is the only active portion of the extension, the subframe gets proper JS
// bindings. See https://crbug.com/40537642.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       ExtensionSubframeGetsBindings) {
  // Load an extension that does not have a background page or popup, so it
  // won't be activated just yet.
  const extensions::Extension* extension =
      LoadExtension(test_data_dir_.AppendASCII("bindings")
                        .AppendASCII("extension_subframe_gets_bindings"));
  ASSERT_TRUE(extension);

  // Navigate current tab to a web URL with a subframe.
  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents,
                            embedded_test_server()->GetURL("/iframe.html")));

  // Navigate the subframe to the extension URL, which should activate the
  // extension.
  GURL extension_url(extension->GetResourceURL("page.html"));
  ResultCatcher catcher;
  content::NavigateIframeToURL(web_contents, "test", extension_url);
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       ExtensionListenersRemoveContext) {
  const Extension* extension = LoadExtension(
      test_data_dir_.AppendASCII("bindings/listeners_destroy_context"));
  ASSERT_TRUE(extension);

  ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);

  // Navigate to a web page with an iframe (the iframe is title1.html).
  GURL main_frame_url = embedded_test_server()->GetURL("a.com", "/iframe.html");
  content::WebContents* tab = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(tab, main_frame_url));

  content::RenderFrameHost* main_frame = tab->GetPrimaryMainFrame();
  content::RenderFrameHost* subframe = ChildFrameAt(main_frame, 0);
  content::RenderFrameDeletedObserver subframe_deleted(subframe);

  // Wait for the extension's content script to be ready.
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // It's actually critical to the test that these frames are in the same
  // process, because otherwise a crash in the iframe wouldn't be detectable
  // (since we rely on JS execution in the main frame to tell if the renderer
  // crashed - see comment below).
  content::RenderProcessHost* main_frame_process = main_frame->GetProcess();
  EXPECT_EQ(main_frame_process, subframe->GetProcess());

  ExtensionTestMessageListener failure_listener("failed");

  // Tell the extension to register listeners that will remove the iframe, and
  // trigger them.
  listener.Reply("go!");

  // The frame will be deleted.
  subframe_deleted.WaitUntilDeleted();

  // Unfortunately, we don't have a good way of checking if something crashed
  // after the frame was removed. WebContents::IsCrashed() seems like it should
  // work, but is insufficient. Instead, use JS execution as the source of
  // true.
  EXPECT_FALSE(tab->IsCrashed());
  EXPECT_EQ(main_frame_url, main_frame->GetLastCommittedURL());
  EXPECT_EQ(main_frame_process, main_frame->GetProcess());
  EXPECT_EQ(true, content::EvalJs(main_frame, "true;"));
  EXPECT_FALSE(failure_listener.was_satisfied());
}

IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, UseAPIsAfterContextRemoval) {
  EXPECT_TRUE(RunExtensionTest("bindings/invalidate_context")) << message_;
}

// Tests that we don't crash if the extension invalidates the context in a
// callback with a runtime.lastError present. Regression test for
// https://crbug.com/41448033.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       InvalidateContextInCallbackWithLastError) {
  TestExtensionDir dir;
  dir.WriteManifest(
      R"({
           "name": "Invalidate Context in onDisconnect",
           "version": "0.1",
           "manifest_version": 2,
           "background": {"scripts": ["background.js"]}
         })");

  constexpr char kFrameHtml[] =
      R"(<html>
           <body></body>
           <script src="frame.js"></script>
         </html>)";
  constexpr char kFrameJs[] =
      R"(chrome.tabs.executeScript({code: ''}, () => {
           // We expect a last error to be present, since we don't have access
           // to the tab.
           chrome.test.assertTrue(!!chrome.runtime.lastError);
           // Remove the frame from the DOM. This causes blink to remove the
           // associated script contexts.
           parent.document.body.removeChild(
               parent.document.body.querySelector('iframe'));
         });)";
  constexpr char kBackgroundJs[] =
      R"(let frame = document.createElement('iframe');
         frame.src = 'frame.html';
         let observer = new MutationObserver((mutationList) => {
           for (let mutation of mutationList) {
             if (mutation.removedNodes.length == 0)
               continue;
             chrome.test.assertEq(1, mutation.removedNodes.length);
             chrome.test.assertEq('IFRAME', mutation.removedNodes[0].tagName);
             chrome.test.notifyPass();
             break;
           }
         });
         observer.observe(document.body, {childList: true});
         document.body.appendChild(frame);)";
  dir.WriteFile(FILE_PATH_LITERAL("frame.html"), kFrameHtml);
  dir.WriteFile(FILE_PATH_LITERAL("frame.js"), kFrameJs);
  dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);

  ResultCatcher catcher;
  const Extension* extension = LoadExtension(dir.UnpackedPath());
  ASSERT_TRUE(extension);
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// TODO(devlin): Can this be combined with
// ExtensionBindingsApiTest.UseAPIsAfterContextRemoval?
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest, UseAppAPIAfterFrameRemoval) {
  ASSERT_TRUE(RunExtensionTest("crazy_extension"));
}

// Tests attaching two listeners from the same extension but different pages,
// then removing one, and ensuring the second is still notified.
// Regression test for https://crbug.com/40587085.
IN_PROC_BROWSER_TEST_F(
    ExtensionBindingsApiTest,
    MultipleEventListenersFromDifferentContextsAndTheSameExtension) {
  // A script that listens for tab creation and populates the result in a
  // global variable.
  constexpr char kTestPageScript[] = R"(
    window.tabEventId = -1;
    function registerListener() {
      chrome.tabs.onCreated.addListener((tab) => {
        window.tabEventId = tab.id;
      });
    }
  )";
  TestExtensionDir test_dir;
  test_dir.WriteManifest(R"(
    {
      "name": "Duplicate event listeners",
      "manifest_version": 2,
      "version": "0.1"
    })");
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"),
                     R"(<html><script src="page.js"></script></html>)");
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kTestPageScript);

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Set up: open two tabs to the same extension page, and wait for each to
  // load.
  const GURL page_url = extension->GetResourceURL("page.html");
  NavigateToURLInNewTab(page_url);
  content::WebContents* first_tab = GetActiveWebContents();
  NavigateToURLInNewTab(page_url);
  content::WebContents* second_tab = GetActiveWebContents();

  // Initially, there are no listeners registered.
  EventRouter* event_router = EventRouter::Get(profile());
  EXPECT_FALSE(event_router->ExtensionHasEventListener(extension->id(),
                                                       "tabs.onCreated"));

  // Register both listeners, and verify they were added.
  ASSERT_TRUE(content::ExecJs(first_tab, "registerListener()"));
  ASSERT_TRUE(content::ExecJs(second_tab, "registerListener()"));
  EXPECT_TRUE(event_router->ExtensionHasEventListener(extension->id(),
                                                      "tabs.onCreated"));

  // Close one of the extension pages.
  content::WebContentsDestroyedWatcher watcher(second_tab);
  CloseTabForWebContents(second_tab);
  watcher.Wait();
  // Hacky round trip to the renderer to flush IPCs.
  ASSERT_TRUE(content::ExecJs(first_tab, ""));

  // Since the second page is still open, the extension should still be
  // registered as a listener.
  EXPECT_TRUE(event_router->ExtensionHasEventListener(extension->id(),
                                                      "tabs.onCreated"));

  // Open a new tab.
  NavigateToURLInNewTab(GURL("chrome://newtab"));
  content::WebContents* new_tab = GetActiveWebContents();

  // The extension should have been notified about the new tab, and have
  // recorded the result.
  int result_tab_id =
      content::EvalJs(first_tab, "window.tabEventId").ExtractInt();
  EXPECT_EQ(sessions::SessionTabHelper::IdForTab(new_tab).id(), result_tab_id);
}

// Verifies that user gestures are carried through extension messages.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       UserGestureFromExtensionMessageTest) {
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
           "name": "User Gesture Content Script",
           "manifest_version": 2,
           "version": "0.1",
           "background": { "scripts": ["background.js"] },
           "content_scripts": [{
             "matches": ["*://*.example.com:*/*"],
             "js": ["content_script.js"],
             "run_at": "document_end"
           }]
         })");
  test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"),
                     R"(const button = document.getElementById('go-button');
                        button.addEventListener('click', () => {
                          chrome.runtime.sendMessage('clicked');
                        });)");
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                     R"(chrome.runtime.onMessage.addListener((message) => {
                        chrome.test.sendMessage(
                            'Clicked: ' +
                            chrome.test.isProcessingUserGesture());
                        });)");

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  const GURL url = embedded_test_server()->GetURL(
      "example.com", "/extensions/page_with_button.html");
  content::WebContents* tab = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(tab, url));

  {
    // Passing a message without an active user gesture shouldn't result in a
    // gesture being active on the receiving end.
    ExtensionTestMessageListener listener;
    EXPECT_EQ(
        content::EvalJs(tab, "document.getElementById('go-button').click()",
                        content::EXECUTE_SCRIPT_NO_USER_GESTURE),
        base::Value());

    EXPECT_TRUE(listener.WaitUntilSatisfied());
    EXPECT_EQ("Clicked: false", listener.message());
  }

  {
    // If there is an active user gesture when the message is sent, we should
    // synthesize a user gesture on the receiving end.
    ExtensionTestMessageListener listener;
    EXPECT_EQ(
        content::EvalJs(tab, "document.getElementById('go-button').click()"),
        base::Value());

    EXPECT_TRUE(listener.WaitUntilSatisfied());
    EXPECT_EQ("Clicked: true", listener.message());
  }
}

// Verifies that user gestures from API calls are active when the callback is
// triggered.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       UserGestureInExtensionAPICallback) {
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
           "name": "User Gesture Extension API Callback",
           "manifest_version": 2,
           "version": "0.1"
         })");
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), "<html></html>");

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  const GURL extension_page = extension->GetResourceURL("page.html");
  content::WebContents* tab = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(tab, extension_page));

  constexpr char kScript[] =
      R"(
          new Promise(resolve => {
            chrome.tabs.query({}, resolve);
          }).then((tabs) => {
           let message;
           if (chrome.runtime.lastError)
             message = 'Unexpected error: ' + chrome.runtime.lastError;
           else
             message = 'Has gesture: ' + chrome.test.isProcessingUserGesture();
           return message;
         });)";

  // Triggering an API without an active gesture shouldn't result in a
  // gesture in the callback.
  EXPECT_EQ(
      "Has gesture: false",
      content::EvalJs(tab, kScript, content::EXECUTE_SCRIPT_NO_USER_GESTURE));
  // If there was an active gesture at the time of the API call, there should
  // be an active gesture in the callback.
  EXPECT_EQ("Has gesture: true", content::EvalJs(tab, kScript));
}

// Tests that a web page can consume a user gesture after an extension sends and
// receives a reply during the same user gesture.
// Regression test for https://crbug.com/41435171.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       WebUserGestureAfterMessagingCallback) {
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
           "name": "User Gesture Messaging Test",
           "version": "0.1",
           "manifest_version": 2,
           "content_scripts": [{
             "matches": ["*://*/*"],
             "js": ["content_script.js"],
             "run_at": "document_start"
           }],
           "background": {
             "scripts": ["background.js"]
           }
         })");
  test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"),
                     R"(window.addEventListener('mousedown', () => {
           chrome.runtime.sendMessage('hello', () => {
             let message = chrome.test.isProcessingUserGesture() ?
                 'got reply' : 'no user gesture';
             chrome.test.sendMessage(message);
           });
         });)");
  test_dir.WriteFile(
      FILE_PATH_LITERAL("background.js"),
      R"(chrome.runtime.onMessage.addListener((message, sender, respond) => {
           respond('reply');
         });
         chrome.test.sendMessage('ready');)");

  const Extension* extension = nullptr;
  {
    ExtensionTestMessageListener listener("ready");
    extension = LoadExtension(test_dir.UnpackedPath());
    ASSERT_TRUE(extension);
    EXPECT_TRUE(listener.WaitUntilSatisfied());
  }

  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL(
          "/extensions/api_test/bindings/user_gesture_test.html")));

  {
    ExtensionTestMessageListener listener("got reply");
    listener.set_failure_message("no user gesture");
    MouseDownInWebContents(web_contents);
    EXPECT_TRUE(listener.WaitUntilSatisfied());
  }

  MouseUpInWebContents(web_contents);

  EXPECT_EQ("success",
            content::EvalJs(web_contents, "window.getEnteredFullscreen",
                            content::EXECUTE_SCRIPT_NO_USER_GESTURE));
}

// Tests that a web page can consume a user gesture after an extension calls a
// method and receives the response in the callback.
// Regression test for https://crbug.com/41435171.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       WebUserGestureAfterApiCallback) {
  TestExtensionDir test_dir;
  test_dir.WriteManifest(
      R"({
           "name": "User Gesture Messaging Test",
           "version": "0.1",
           "manifest_version": 2,
           "content_scripts": [{
             "matches": ["*://*/*"],
             "js": ["content_script.js"],
             "run_at": "document_start"
           }],
           "permissions": ["storage"]
         })");
  test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"),
                     R"(window.addEventListener('mousedown', () => {
           chrome.storage.local.get('foo', () => {
             let message = chrome.test.isProcessingUserGesture() ?
                 'got reply' : 'no user gesture';
             chrome.test.sendMessage(message);
           });
         });)");

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(web_contents);
  ASSERT_TRUE(NavigateToURL(
      web_contents,
      embedded_test_server()->GetURL(
          "/extensions/api_test/bindings/user_gesture_test.html")));

  {
    ExtensionTestMessageListener listener("got reply");
    listener.set_failure_message("no user gesture");
    MouseDownInWebContents(web_contents);
    EXPECT_TRUE(listener.WaitUntilSatisfied());
  }

  MouseUpInWebContents(web_contents);

  EXPECT_EQ("success",
            content::EvalJs(web_contents, "window.getEnteredFullscreen",
                            content::EXECUTE_SCRIPT_NO_USER_GESTURE));
}

// Desktop Android does not support side panel yet. https://crbug.com/405218955
#if BUILDFLAG(ENABLE_EXTENSIONS)
// Tests that, if a document has an active user gesture, a new message coming in
// that *also* has a user gesture won't override the active one. Regression test
// for https://crbug.com/355266358.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       UserGestureFromMessageWontOverrideActiveGesture) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "version": "0.1",
           "background": {"service_worker": "background.js"},
           "side_panel": {"default_path": "panel.html"},
           "content_scripts": [{
             "matches": ["*://example.com/*"],
             "js": ["content_script.js"]
           }],
           "permissions": ["sidePanel"]
         })";
  // The following content script:
  // 1) Installs a listener for mousedown that sends a message to the
  //    service worker.
  // 2) Has a listener for a response from the service worker.
  // 3) Has a listener for a click event (mousedown -> mouseup) that
  //    waits for the response from the service worker, and then sends a new
  //    message instructing the service worker to open the side panel.
  // In step 1), there will be an active user gesture. Step 2) will receive
  // a message and that message will *also* have an active user gesture, but
  // it is "restricted" since it came from an extension message, rather than a
  // user interaction. As such, the user gesture from step 2) cannot be used to
  // curry a user gesture further along in messaging. But the user gesture from
  // step 1) should still be active at this point, and is valid, so that should
  // result in a user gesture being sent with the message in step 3), which
  // allows the side panel to be opened.
  static constexpr char kContentScriptJs[] =
      R"(let resolveResponsePromise;
         let receivedResponse = new Promise((resolve) => {
           resolveResponsePromise = resolve;
         });
         document.addEventListener('mousedown', () => {
           chrome.runtime.sendMessage('first');
         });
         chrome.runtime.onMessage.addListener(msg => {
           chrome.test.assertEq('firstResponse', msg);
           resolveResponsePromise();
         });
         document.addEventListener('click', async() => {
           await receivedResponse;
           chrome.runtime.sendMessage('openPanel');
         });
         chrome.test.sendMessage('content script ready');)";
  // The service worker listens for incoming messages. It will reply to the
  // first and open the side panel on the second.
  static constexpr char kBackgroundJs[] =
      R"(chrome.runtime.onMessage.addListener((msg, sender) => {
           if (msg == 'first') {
             chrome.tabs.sendMessage(sender.tab.id, 'firstResponse');
           } else if (msg == 'openPanel') {
             chrome.sidePanel.open({tabId: sender.tab.id}, () => {
               chrome.test.assertNoLastError();
             });
           } else {
             chrome.test.fail('Unexpected message: ' + msg);
           }
         });)";
  // A side panel and accompanying script that simply call notifyPass() on
  // opening.
  static constexpr char kPanelHtml[] =
      R"(<html>
           Hello, world!
           <script src="panel.js"></script>
         </html>)";
  static constexpr char kPanelJs[] = "chrome.test.notifyPass()";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kContentScriptJs);
  test_dir.WriteFile(FILE_PATH_LITERAL("panel.html"), kPanelHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("panel.js"), kPanelJs);

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  const GURL url =
      embedded_test_server()->GetURL("example.com", "/simple.html");

  // Navigate to a web page and wait for the content script to run and set up.
  ExtensionTestMessageListener content_script_listener("content script ready");
  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, url));
  ASSERT_TRUE(content_script_listener.WaitUntilSatisfied());

  ResultCatcher result_catcher;
  // Click in the web contents.
  MouseDownInWebContents(web_contents);
  MouseUpInWebContents(web_contents);

  // The test succeeds if the side panel opens.
  ASSERT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

// Tests that bindings are properly instantiated for a window navigated to an
// extension URL after being opened with an undefined URL.
// Regression test for https://crbug.com/40611007.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       TestBindingsAvailableWithNavigatedBlankWindow) {
  constexpr char kManifest[] =
      R"({
           "name": "chrome.runtime bug checker",
           "description": "test case for crbug.com/40611007",
           "version": "0",
           "manifest_version": 2
         })";
  constexpr char kOpenerHTML[] =
      R"(<!DOCTYPE html>
         <html>
           <head>
             <script src='opener.js'></script>
           </head>
           <body>
           </body>
         </html>)";
  // opener.js opens a blank window and then navigates it to an extension URL
  // (where extension APIs should be available).
  constexpr char kOpenerJS[] =
      R"(const url = chrome.runtime.getURL('/page.html');
         const win = window.open(undefined, '');
         win.location = url;
         chrome.test.notifyPass())";
  constexpr char kPageHTML[] =
      R"(<!DOCTYPE html>
         <html>
           This space intentionally left blank.
         </html>)";
  TestExtensionDir extension_dir;
  extension_dir.WriteManifest(kManifest);
  extension_dir.WriteFile(FILE_PATH_LITERAL("opener.html"), kOpenerHTML);
  extension_dir.WriteFile(FILE_PATH_LITERAL("opener.js"), kOpenerJS);
  extension_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHTML);

  const Extension* extension = LoadExtension(extension_dir.UnpackedPath());
  const GURL target_url = extension->GetResourceURL("page.html");

  ResultCatcher catcher;
  content::TestNavigationObserver observer(target_url);
  observer.StartWatchingNewWebContents();
  content::WebContents* web_contents = GetActiveWebContents();
  ASSERT_TRUE(
      NavigateToURL(web_contents, extension->GetResourceURL("opener.html")));
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
  observer.Wait();
  EXPECT_TRUE(observer.last_navigation_succeeded());

  web_contents = GetActiveWebContents();
  EXPECT_EQ(target_url, web_contents->GetLastCommittedURL());

  // Check whether bindings are available. They should be.
  constexpr char kScript[] =
      R"(let message;
         if (!chrome.runtime)
           message = 'Runtime not defined';
         else if (!chrome.tabs)
           message = 'Tabs not defined';
         else
           message = 'success';
         message;)";
  EXPECT_EQ("success", content::EvalJs(web_contents, kScript));
}

// Tests the aliasing of chrome.extension methods to their chrome.runtime
// equivalents.
IN_PROC_BROWSER_TEST_F(ExtensionBindingsApiTest,
                       ChromeExtensionIsAliasedToChromeRuntime) {
  constexpr char kManifest[] =
      R"({
           "name": "Test",
           "version": "0.1",
           "manifest_version": 2,
           "background": { "scripts": ["background.js"] }
         })";
  constexpr char kBackground[] =
      R"(chrome.test.runTests([
           function chromeExtensionIsAliased() {
             // Sanity check: chrome.extension is directly aliased to
             // chrome.runtime.
             chrome.test.assertTrue(!!chrome.runtime);
             chrome.test.assertTrue(!!chrome.runtime.sendMessage);
             chrome.test.assertEq(chrome.runtime.sendMessage,
                                  chrome.extension.sendMessage);
             chrome.test.succeed();
           },
           function testOverridingFailsGracefully() {
             let intercepted = false;
             // Modify the chrome.runtime object, which is the source for the
             // chrome.extension API, to throw an error when sendMessage is
             // accessed. Nothing should blow up.
             // Regression test for https://crbug.com/41450968.
             Object.defineProperty(
                 chrome.runtime,
                 'sendMessage',
                 {
                   get() {
                     intercepted = true;
                     throw new Error('Mwahaha');
                   }
                 });
             chrome.extension.sendMessage;
             chrome.test.assertTrue(intercepted);
             chrome.test.succeed();
           }
         ]);)";

  TestExtensionDir extension_dir;
  extension_dir.WriteManifest(kManifest);
  extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackground);
  ResultCatcher catcher;
  ASSERT_TRUE(LoadExtension(extension_dir.UnpackedPath()));
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

}  // namespace
}  // namespace extensions
