// 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 "chrome/browser/ui/views/extensions/extension_popup.h"

#include "base/strings/strcat.h"
#include "base/test/run_until.h"
#include "chrome/browser/devtools/devtools_window.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_view_host.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
#include "chrome/browser/ui/views/extensions/extensions_toolbar_desktop.h"
#include "chrome/browser/ui/views/extensions/security_dialog_tracker.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/permissions/chip/permission_chip_view.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/permissions/permission_request_manager_test_api.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/javascript_dialogs/app_modal_dialog_controller.h"
#include "components/javascript_dialogs/app_modal_dialog_queue.h"
#include "components/permissions/request_type.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_devtools_protocol_client.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "extensions/test/test_extension_dir.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/test/widget_activation_waiter.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/window/dialog_delegate.h"

namespace {

// A helper class for waiting until the devtools is attached to the given
// WebContents.
class DevToolsAttachWaiter : public content::DevToolsAgentHostObserver {
 public:
  explicit DevToolsAttachWaiter(content::WebContents* web_contents)
      : web_contents_(web_contents) {
    content::DevToolsAgentHost::AddObserver(this);
  }
  ~DevToolsAttachWaiter() override {
    content::DevToolsAgentHost::RemoveObserver(this);
  }

  void Wait() {
    if (content::DevToolsAgentHost::HasFor(web_contents_)) {
      return;
    }
    run_loop_.Run();
  }

  // content::DevToolsAgentHostObserver:
  void DevToolsAgentHostAttached(
      content::DevToolsAgentHost* agent_host) override {
    if (agent_host->GetWebContents() == web_contents_) {
      run_loop_.Quit();
    }
  }

 private:
  base::RunLoop run_loop_;
  raw_ptr<content::WebContents> web_contents_;
};

views::UniqueWidgetPtr CreateTestTopLevelWidget() {
  views::UniqueWidgetPtr widget = std::make_unique<views::Widget>();
  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);
  widget->Init(std::move(params));
  widget->widget_delegate()->SetCanActivate(true);
  return widget;
}

void ExpectWidgetDestroy(base::WeakPtr<views::Widget> widget) {
  if (widget) {
    views::test::WidgetDestroyedWaiter(widget.get()).Wait();
  }
  EXPECT_TRUE(widget.WasInvalidated() || widget->IsClosed());
}

base::WeakPtr<views::Widget> WaitForLastExtensionPopupVisible() {
  EXPECT_NE(ExtensionPopup::last_popup_for_testing(), nullptr);
  base::WeakPtr<views::Widget> extension_popup_widget =
      ExtensionPopup::last_popup_for_testing()->GetWidget()->GetWeakPtr();
  EXPECT_TRUE(extension_popup_widget);
  // Ensure the popup is visible (it shows asynchronously from resource load).
  // This is safe to call even if the widget is already visible.
  views::test::WidgetVisibleWaiter(extension_popup_widget.get()).Wait();
  EXPECT_TRUE(extension_popup_widget->IsVisible());
  return extension_popup_widget;
}

base::WeakPtr<views::Widget> OpenExtensionPopup(
    Browser* browser,
    const extensions::Extension* extension) {
  extensions::ExtensionHostTestHelper popup_waiter(browser->GetProfile(),
                                                   extension->id());
  popup_waiter.RestrictToType(extensions::mojom::ViewType::kExtensionPopup);
  ExtensionActionTestHelper::Create(browser)->Press(extension->id());
  popup_waiter.WaitForHostCompletedFirstLoad();
  return WaitForLastExtensionPopupVisible();
}

}  // namespace

class ExtensionPopupInteractiveUiTest : public extensions::ExtensionApiTest {
 public:
  // Create a dialog widget as a child of `parent` widget.
  static views::UniqueWidgetPtr CreateTestDialogWidget(views::Widget* parent) {
    auto dialog_delegate = std::make_unique<views::DialogDelegateView>(
        views::DialogDelegateView::CreatePassKey());
    return std::unique_ptr<views::Widget>(
        views::DialogDelegate::CreateDialogWidget(dialog_delegate.release(),
                                                  gfx::NativeWindow(),
                                                  parent->GetNativeView()));
  }
};

// Tests unloading an extension while its popup is actively under inspection.
// Regression test for https://crbug.com/40826546.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       UnloadExtensionWhileInspectingPopup) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
                     "<html>Hello, world!</html>");
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Inspect the popup and wait for it to open.
  {
    extensions::ExtensionHostTestHelper popup_waiter(profile(),
                                                     extension->id());
    popup_waiter.RestrictToType(extensions::mojom::ViewType::kExtensionPopup);
    ExtensionActionTestHelper::Create(browser())->InspectPopup(extension->id());
    popup_waiter.WaitForHostCompletedFirstLoad();
  }

  // Unload the extension. This causes the popup's ExtensionHost to be
  // destroyed. This should be safe.
  {
    extensions::ExtensionHostTestHelper popup_waiter(profile(),
                                                     extension->id());
    popup_waiter.RestrictToType(extensions::mojom::ViewType::kExtensionPopup);
    extension_registrar()->DisableExtension(
        extension->id(), {extensions::disable_reason::DISABLE_USER_ACTION});
    popup_waiter.WaitForHostDestroyed();
  }
}

// Tests that the extension popup does not render over an anchored permissions
// bubble. Regression test for https://crbug.com/40058873.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       ExtensionPopupOverPermissions) {
  // Geolocation requires HTTPS. Since we programmatically show the geolocation
  // prompt from C++ (rather than triggering the web API), this might not be
  // strictly necessary, but is nice to have.
  UseHttpsTestServer();
  ASSERT_TRUE(StartEmbeddedTestServer());

  // Load an extension with a popup.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
                     "<html>Hello, world!</html>");
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Navigate to an https site.
  const GURL secure_url =
      embedded_test_server()->GetURL("example.com", "/simple.html");
  EXPECT_EQ("https", secure_url.GetScheme());

  content::RenderFrameHost* frame =
      ui_test_utils::NavigateToURL(browser(), secure_url);
  ASSERT_TRUE(frame);

  // Show the geolocation permissions prompt.
  test::PermissionRequestManagerTestApi permissions_api(browser());
  ASSERT_TRUE(permissions_api.manager());
  permissions_api.AddSimpleRequest(frame,
                                   permissions::RequestType::kGeolocation);
  // Sadly, there's no notification for these. All tests seem to rely on
  // RunUntilIdle() being sufficient.
  // TODO(crbug.com/40835018): Change this to be more deterministic.
  base::RunLoop().RunUntilIdle();

  // The permission may be shown using a chip UI instead of a popped-up bubble.
  // If so, click on the chip to open the bubble.
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  LocationBar* lb = browser_view->toolbar()->location_bar();
  if (lb->GetChipController()->IsPermissionPromptChipVisible() &&
      !lb->GetChipController()->IsBubbleShowing()) {
    views::test::ButtonTestApi(
        views::AsViewClass<views::Button>(
            views::ElementTrackerViews::GetInstance()->GetFirstMatchingView(
                PermissionChipView::kPermissionRequestChipElementId,
                views::ElementTrackerViews::GetContextForView(browser_view))))
        .NotifyClick(ui::MouseEvent(ui::EventType::kMousePressed, gfx::Point(),
                                    gfx::Point(), ui::EventTimeForNow(),
                                    ui::EF_LEFT_MOUSE_BUTTON, 0));
    base::RunLoop().RunUntilIdle();
  }

  // The permissions bubble should now be showing.
  ASSERT_TRUE(permissions_api.GetPromptWindow());

  base::WeakPtr<views::Widget> extension_popup_widget =
      OpenExtensionPopup(browser(), extension);

  // Finally, verify that the extension popup is not on top of the permissions
  // bubble.
  const bool is_stacked_above = views::test::WidgetTest::IsWindowStackedAbove(
      extension_popup_widget.get(), permissions_api.GetPromptWindow());

  EXPECT_FALSE(is_stacked_above);
}

// Tests that an extension popup does not close on deactivation while it is
// under inspection, when the popup was opened with inspection enabled.
// TODO(crbug.com/478799302): Flakily fails on TSAN bots
#if defined(THREAD_SANITIZER)
#define MAYBE_ExtensionPopupDoesNotCloseWhileInspecting \
  DISABLED_ExtensionPopupDoesNotCloseWhileInspecting
#else
#define MAYBE_ExtensionPopupDoesNotCloseWhileInspecting \
  ExtensionPopupDoesNotCloseWhileInspecting
#endif
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       MAYBE_ExtensionPopupDoesNotCloseWhileInspecting) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
                     "<html>Hello, world!</html>");
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Open the popup and inspect it.
  extensions::ExtensionHostTestHelper popup_waiter(profile(), extension->id());
  popup_waiter.RestrictToType(extensions::mojom::ViewType::kExtensionPopup);
  ExtensionActionTestHelper::Create(browser())->InspectPopup(extension->id());
  popup_waiter.WaitForHostCompletedFirstLoad();
  content::WebContents* extension_contents =
      ExtensionPopup::last_popup_for_testing()->host()->host_contents();
  ASSERT_NE(extension_contents, nullptr);
  DevToolsAttachWaiter(extension_contents).Wait();

  base::WeakPtr<views::Widget> extension_popup_widget =
      ExtensionPopup::last_popup_for_testing()->GetWidget()->GetWeakPtr();
  ASSERT_TRUE(extension_popup_widget);
  // Ensure the popup is visible (it shows asynchronously from resource load).
  // This is safe to call even if the widget is already visible.
  views::test::WidgetVisibleWaiter(extension_popup_widget.get()).Wait();
  EXPECT_TRUE(extension_popup_widget->IsVisible());

  // Deactivating the extension popup should not cause the extension popup to
  // close. Note that some platforms don't implement Widget::Deactivate() so we
  // activate another window (the browser window in this case) to trigger that.
  extension_popup_widget->Activate();
  // However, on Linux activating the browser window does not cause the
  // extension popup to deactivate, thus we also explicitly call Deactivate().
  extension_popup_widget->Deactivate();
  browser()->GetWindow()->Activate();
  views::test::WaitForWidgetActive(extension_popup_widget.get(), false);
  ASSERT_TRUE(extension_popup_widget);
  EXPECT_TRUE(extension_popup_widget->IsVisible());

  // Reactivates the extension popup.
  ExtensionPopup::last_popup_for_testing()->GetWidget()->Activate();

  // Stop inspecting the extension popup.
  ASSERT_TRUE(content::DevToolsAgentHost::HasFor(extension_contents));
  scoped_refptr<content::DevToolsAgentHost> devtools_agent_host =
      content::DevToolsAgentHost::GetOrCreateFor(extension_contents);
  devtools_agent_host->DetachAllClients();

  // Activating the browser window should cause the extension popup to be
  // deactivated and closed.
  browser()->GetWindow()->Activate();
  ExpectWidgetDestroy(extension_popup_widget);
}

// Tests that an extension popup does not close on deactivation when it shows
// an app modal dialog. The JS alert dialog is used as surrogate.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       ExtensionPopupDoesNotCloseWhileShowingAppModal) {
  // Install a test extension that opens an alert() dialog.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";
  // Use setTimeout() to create the JS alert dialog, otherwise
  // WaitForHostCompletedFirstLoad() won't return due to the open dialog.
  static constexpr char kPageJS[] =
      R"(window.onload = function() {
           setTimeout(alert, 0);
         })";
  static constexpr char kHTML[] =
      R"(<html><script src="page.js"></script></html>)";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJS);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kHTML);
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  base::WeakPtr<views::Widget> extension_popup_widget =
      OpenExtensionPopup(browser(), extension);

  // Wait until the web modal dialog is shown.
  javascript_dialogs::AppModalDialogController* dialog =
      ui_test_utils::WaitForAppModalDialog();
  ASSERT_NE(dialog, nullptr);
  // The extension popup should be still showing.
  ASSERT_TRUE(extension_popup_widget);
  EXPECT_TRUE(extension_popup_widget->IsVisible());

  // Close the JS dialog and deactivate the extension popup.
  dialog->CloseModalDialog();
  // Activating the browser window should cause the extension popup to be
  // deactivated and closed.
  browser()->GetWindow()->Activate();

  // The extension popup should close.
  ExpectWidgetDestroy(extension_popup_widget);
}

// TODO(crbug.com/378966968): timeout on win-rel but not reproducible locally.
#if !BUILDFLAG(IS_WIN)
// Tests that an extension popup does not close on deactivation when it shows
// an web modal dialog. The webauthn dialog is used as surrogate.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       ExtensionPopupDoesNotCloseWhileShowingWebModal) {
  // Install a test extension that opens an alert() dialog.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";
  // Use setTimeout() to create the Webauthn dialog because this dialog
  // requests the web contents to be in focus.
  static constexpr char kPageJS[] =
      R"(function createCreds() {
          navigator.credentials.create({ publicKey: {
          rp: { name: "" },
          user: { id: new Uint8Array([0]), name: "foo", displayName: "" },
          pubKeyCredParams: [{type: "public-key", alg: -7}],
          challenge: new Uint8Array([0]),
          timeout: 10000,
          userVerification: 'discouraged',}});
         }
         window.onload = () => setTimeout(createCreds, 0);
        )";
  static constexpr char kHTML[] =
      R"(<html><script src="page.js"></script></html>)";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJS);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kHTML);
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  base::WeakPtr<views::Widget> extension_popup_widget =
      OpenExtensionPopup(browser(), extension);

  content::WebContents* extension_contents =
      ExtensionPopup::last_popup_for_testing()->host()->host_contents();
  ASSERT_TRUE(extension_contents);
  extension_contents->Focus();

  // Wait until the web modal dialog is shown.
  ui_test_utils::WaitForWebModalDialog(extension_contents);
  // The extension popup should be still showing.
  ASSERT_TRUE(extension_popup_widget);
  EXPECT_TRUE(extension_popup_widget->IsVisible());

  // Close the JS dialog and deactivate the extension popup.
  web_modal::WebContentsModalDialogManager* modal_manager =
      web_modal::WebContentsModalDialogManager::FromWebContents(
          extension_contents);
  ASSERT_TRUE(modal_manager);
  web_modal::WebContentsModalDialogManager::TestApi(modal_manager)
      .CloseAllDialogs();
  // Activating the browser window should cause the extension popup to be
  // deactivated and closed.
  browser()->GetWindow()->Activate();

  // The extension popup should close.
  ExpectWidgetDestroy(extension_popup_widget);
}
#endif  // !BUILDFLAG(IS_WIN)

// Tests that an extension popup is closed when a web dialog is shown as active.
// In this test the web dialog is not initiated by the extension popup (see
// ExtensionPopupDoesNotCloseWhileShowingAppModal for showing an extension-owned
// dialog).
// This prevents the extension from occluding the web dialog, which can be
// dangerous when the web dialog is a security-related dialog (e.g. screen
// sharing dialog, webauthn attestation dialog).
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       ExtensionPopupClosesOnShowingWebDialog) {
  // Install a test extension.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
                     "<html>Hello, world!</html>");
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  base::WeakPtr<views::Widget> extension_popup_widget =
      OpenExtensionPopup(browser(), extension);

  // Show a web dialog.
  auto web_dialog = std::make_unique<views::DialogDelegateView>(
      views::DialogDelegateView::CreatePassKey());
  web_dialog->SetPreferredSize(gfx::Size(100, 100));
  web_dialog->SetModalType(ui::mojom::ModalType::kChild);
  web_dialog->SetCanActivate(true);
  views::Widget* web_dialog_widget =
      constrained_window::ShowWebModalDialogViews(
          web_dialog.release(),
          browser()->GetTabStripModel()->GetActiveWebContents());
  views::test::WidgetVisibleWaiter(web_dialog_widget).Wait();
  EXPECT_TRUE(web_dialog_widget->IsVisible());

  // Check that the extension popup is closed.
  ExpectWidgetDestroy(extension_popup_widget);
}

// Tests that an extension popup closes when activating the browser window.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       ExtensionPopupClosesOnActivatingBrowserWindow) {
  // Install a test extension.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
                     "<html>Hello, world!</html>");
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  base::WeakPtr<views::Widget> extension_popup_widget =
      OpenExtensionPopup(browser(), extension);

  // Activate the browser window should close the extension popup.
  browser()->GetWindow()->Activate();
  ExpectWidgetDestroy(extension_popup_widget);
}

#if BUILDFLAG(IS_MAC)
// Tests that an extension popup closes when activating the browser window
// in macOS fullscreen.
// TODO(crbug.com/514169818): Re-enable the test
IN_PROC_BROWSER_TEST_F(
    ExtensionPopupInteractiveUiTest,
    DISABLED_ExtensionPopupClosesOnActivatingBrowserWindowMacFullscreen) {
  // Install a test extension.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
                     "<html>Hello, world!</html>");
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  ui_test_utils::ToggleFullscreenModeAndWait(browser());

  base::WeakPtr<views::Widget> extension_popup_widget =
      OpenExtensionPopup(browser(), extension);

  // Activate the browser window should close the extension popup.
  browser()->GetWindow()->Activate();
  ExpectWidgetDestroy(extension_popup_widget);
}
#endif

// Tests that an extension popup does not close when activating an unrelated
// top-level widget. This behavior is useful for users who want to keep the
// popup open to look at the info there while working on other apps or browser
// windows.
IN_PROC_BROWSER_TEST_F(
    ExtensionPopupInteractiveUiTest,
    ExtensionPopupDoesNotClosesOnActivatingOtherTopLevelWindow) {
  // Install a test extension.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"),
                     "<html>Hello, world!</html>");
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  base::WeakPtr<views::Widget> extension_popup_widget =
      OpenExtensionPopup(browser(), extension);

  // Activate a different top-level widget should not close the extension popup.
  views::UniqueWidgetPtr widget = CreateTestTopLevelWidget();
  widget->Show();
  views::test::WaitForWidgetActive(widget.get(), true);

  ASSERT_NE(extension_popup_widget, nullptr);
  EXPECT_FALSE(extension_popup_widget->IsClosed());
}

// Tests that attaching an external devtools client to an extension popup while
// it is loading does not open a devtools window automatically. Regression test
// for crbug.com/537973327.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       SlowLoadingExtensionPopupDoesNotOpenDevToolsWindow) {
  // Start an embedded test server that serves a slow responding image.
  static constexpr char kSlowImgURL[] = "/slow-img";
  net::test_server::ControllableHttpResponse slow_img_response(
      embedded_test_server(), kSlowImgURL);
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL slow_image_url = embedded_test_server()->GetURL(kSlowImgURL);

  // Load an extension whose popup embeds the slow loading image.
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";
  const std::string html =
      base::StrCat({"<html><img src='", slow_image_url.spec(), "'></html>"});

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), html);

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

  // Use the raw action to open the popup because we control its loading.
  ExtensionActionTestHelper::Create(browser())->Press(extension->id());

  // Start the request but do not finish it.
  slow_img_response.WaitForRequest();

  // Attach a devtools client to the popup.
  ExtensionPopup* extension_popup = ExtensionPopup::last_popup_for_testing();
  ASSERT_TRUE(extension_popup);
  content::WebContents* extension_contents =
      extension_popup->host()->host_contents();
  ASSERT_TRUE(extension_contents);

  // This emulates what chromedriver does when enumerating
  // driver.window_handles.
  content::TestDevToolsProtocolClient devtools_client;
  devtools_client.AttachToTabTarget(extension_contents);
  base::DictValue command_params;
  command_params.Set("autoAttach", true);
  command_params.Set("waitForDebuggerOnStart", false);
  command_params.Set("flatten", true);
  devtools_client.SendCommandSync("Target.setAutoAttach",
                                  std::move(command_params));
  // Sanity check: ensure "Target.setAutoAttach" was sent before the widget got
  // shown.
  extension_popup = ExtensionPopup::last_popup_for_testing();
  ASSERT_TRUE(extension_popup);
  ASSERT_FALSE(extension_popup->GetWidget()->IsVisible());

  // Respond to the image loading.
  slow_img_response.Send(net::HTTP_OK, "image/png");
  slow_img_response.Send("image_body");
  slow_img_response.Done();

  base::WeakPtr<views::Widget> extension_popup_widget =
      WaitForLastExtensionPopupVisible();

  // Primary assertion of the test: ensure no devtools window opened.
  EXPECT_FALSE(
      DevToolsWindow::GetInstanceForInspectedWebContents(extension_contents));

  // Cleanup.
  devtools_client.DetachProtocolClient();
  // Activating the browser window should cause the extension popup to be
  // deactivated and closed.
  browser()->GetWindow()->Activate();

  // The extension popup should close.
  ExpectWidgetDestroy(extension_popup_widget);
}

// Tests that an API-triggered extenion popup does not show if a security dialog
// is visible. This extension loads a slow image that completes loading only
// after a security dialog is shown.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       APITriggeredPopupIsBlockedBySecurityDialog) {
  // Start an embedded test server that serves a slow responding image.
  static constexpr char kSlowImgURL[] = "/slow-img";
  net::test_server::ControllableHttpResponse slow_img_response(
      embedded_test_server(), kSlowImgURL);
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL slow_image_url = embedded_test_server()->GetURL(kSlowImgURL);

  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";
  const std::string html =
      base::StrCat({"<html><img src='", slow_image_url.spec(), "'></html>"});

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), html);
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Try to open an extension by API.
  extensions::ExtensionHostTestHelper popup_waiter(browser()->GetProfile(),
                                                   extension->id());
  popup_waiter.RestrictToType(extensions::mojom::ViewType::kExtensionPopup);
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  ExtensionsToolbarDesktop* extensions_container =
      browser_view->toolbar()->extensions_container();
  extensions_container->GetToolbarViewModel()->ShowToolbarActionPopupForAPICall(
      extension->id(), ShowPopupCallback());

  // The extension should load the image.
  slow_img_response.WaitForRequest();

  // While the extension is loading, open a security UI.
  views::UniqueWidgetPtr security_widget =
      CreateTestDialogWidget(browser_view->GetWidget());
  extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
      security_widget.get());
  security_widget->Show();
  views::test::WidgetVisibleWaiter(security_widget.get()).Wait();

  // Respond to the image loading.
  slow_img_response.Send(net::HTTP_OK, "image/png");
  slow_img_response.Send("image_body");
  slow_img_response.Done();

  // The extension should be destroyed without showing.
  popup_waiter.WaitForHostDestroyed();
}

// Tests that a user-triggered extenion popup is blocked by visible security
// dialogs.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       UserTriggeredPopupIsBlockedBySecurityUI) {
  // Start an embedded test server that serves a slow responding image.
  static constexpr char kSlowImgURL[] = "/slow-img";
  net::test_server::ControllableHttpResponse slow_img_response(
      embedded_test_server(), kSlowImgURL);
  ASSERT_TRUE(StartEmbeddedTestServer());
  const GURL slow_image_url = embedded_test_server()->GetURL(kSlowImgURL);

  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";
  const std::string html =
      base::StrCat({"<html><img src='", slow_image_url.spec(), "'></html>"});

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), html);
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Try to open an extension.
  extensions::ExtensionHostTestHelper popup_waiter(browser()->GetProfile(),
                                                   extension->id());
  popup_waiter.RestrictToType(extensions::mojom::ViewType::kExtensionPopup);
  ExtensionActionTestHelper::Create(browser())->Press(extension->id());

  // The extension should load the image.
  slow_img_response.WaitForRequest();

  // While the extension is loading, open a security UI.
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  views::UniqueWidgetPtr security_widget =
      CreateTestDialogWidget(browser_view->GetWidget());
  extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
      security_widget.get());
  security_widget->Show();
  views::test::WidgetVisibleWaiter(security_widget.get()).Wait();

  // Respond to the image loading.
  slow_img_response.Send(net::HTTP_OK, "image/png");
  slow_img_response.Send("image_body");
  slow_img_response.Done();

  // The extension should be destroyed without showing.
  popup_waiter.WaitForHostDestroyed();
}

// Tests that pressing Escape in an extension popup does not close the popup
// if the keydown event is default-prevented by JavaScript.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       EscapeKeyDoesNotClosePopupWhenPreventedInJS) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";
  static constexpr char kPopupHtml[] =
      R"(<!DOCTYPE html>
         <html>
         <body>
           <script src="popup.js"></script>
         </body>
         </html>)";
  static constexpr char kPopupJs[] =
      R"(window.escapeHandled = false;
         window.addEventListener('keydown', (e) => {
           if (e.key === 'Escape') {
             e.preventDefault();
           }
         });
         window.addEventListener('keyup', (e) => {
           if (e.key === 'Escape') {
             window.escapeHandled = true;
           }
         });)";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kPopupHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.js"), kPopupJs);
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  base::WeakPtr<views::Widget> widget =
      OpenExtensionPopup(browser(), extension);
  ASSERT_TRUE(widget);

  content::WebContents* host_contents =
      ExtensionPopup::last_popup_for_testing()->host()->host_contents();

  // Send Escape key press directly to the popup bubble window.
  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      widget->GetNativeWindow(), ui::VKEY_ESCAPE, false, false, false, false));

  // Wait for JavaScript to process the key event.
  EXPECT_TRUE(base::test::RunUntil([&]() -> bool {
    return content::EvalJs(host_contents, "window.escapeHandled")
               .ExtractBool() == true;
  }));

  // The popup widget should still be open and visible.
  EXPECT_TRUE(widget);
  EXPECT_FALSE(widget->IsClosed());
}

// Tests that pressing Escape in an extension popup closes the popup when not
// default-prevented by JavaScript.
IN_PROC_BROWSER_TEST_F(ExtensionPopupInteractiveUiTest,
                       EscapeKeyClosesPopupWhenNotPreventedInJS) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test Extension",
           "manifest_version": 3,
           "action": { "default_popup": "popup.html" },
           "version": "0.1"
         })";
  static constexpr char kPopupHtml[] = "<html><body>Popup</body></html>";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kPopupHtml);
  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  base::WeakPtr<views::Widget> widget =
      OpenExtensionPopup(browser(), extension);
  ASSERT_TRUE(widget);

  // Send Escape key press directly to the popup bubble window.
  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      widget->GetNativeWindow(), ui::VKEY_ESCAPE, false, false, false, false));

  // Wait until the popup widget is destroyed.
  EXPECT_TRUE(base::test::RunUntil([&]() { return !widget; }));
}
