chromium/chrome/browser/ui/views/select_file_dialog_extension_browsertest.cc

// 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.

#include "chrome/browser/ui/views/select_file_dialog_extension.h"

#include <memory>

#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/keyboard/keyboard_switches.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_callback_support.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "chrome/browser/ash/file_manager/file_manager_test_util.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/policy/dlp/dlp_files_controller_ash.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_file_destination.h"
#include "chrome/browser/chromeos/policy/dlp/dlp_rules_manager_factory.h"
#include "chrome/browser/chromeos/policy/dlp/test/mock_dlp_rules_manager.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_factory.h"
#include "extensions/browser/process_manager.h"
#include "extensions/test/extension_background_page_waiter.h"
#include "extensions/test/extension_test_message_listener.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/window.h"
#include "ui/gfx/color_palette.h"
#include "ui/shell_dialogs/select_file_dialog.h"
#include "ui/shell_dialogs/select_file_policy.h"
#include "ui/shell_dialogs/selected_file_info.h"

namespace {

class KeyboardVisibleWaiter : public ChromeKeyboardControllerClient::Observer {
 public:
  KeyboardVisibleWaiter() {
    ChromeKeyboardControllerClient::Get()->AddObserver(this);
  }
  ~KeyboardVisibleWaiter() override {
    ChromeKeyboardControllerClient::Get()->RemoveObserver(this);
  }

  void Wait() { run_loop_.Run(); }

  // ChromeKeyboardControllerClient::Observer
  void OnKeyboardVisibilityChanged(bool visible) override {
    if (visible)
      run_loop_.QuitWhenIdle();
  }

 private:
  base::RunLoop run_loop_;
};

}  // namespace

class MockSelectFileDialogListener : public ui::SelectFileDialog::Listener {
 public:
  MockSelectFileDialogListener() = default;

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

  bool file_selected() const { return file_selected_; }
  bool canceled() const { return canceled_; }
  base::FilePath path() const { return path_; }

  // ui::SelectFileDialog::Listener:
  void FileSelected(const ui::SelectedFileInfo& file, int index) override {
    file_selected_ = true;
    path_ = file.path();
    QuitMessageLoop();
  }
  void MultiFilesSelected(
      const std::vector<ui::SelectedFileInfo>& files) override {
    QuitMessageLoop();
  }
  void FileSelectionCanceled() override {
    canceled_ = true;
    QuitMessageLoop();
  }

  void WaitForCalled() {
    message_loop_runner_ = new content::MessageLoopRunner();
    message_loop_runner_->Run();
  }

 private:
  void QuitMessageLoop() {
    if (message_loop_runner_.get())
      message_loop_runner_->Quit();
  }

  bool file_selected_ = false;
  bool canceled_ = false;
  base::FilePath path_;
  scoped_refptr<content::MessageLoopRunner> message_loop_runner_;
};

// Parametrization of tests. We run tests with and
// without filt type filter enabled, and in tablet mode and in regular mode.
struct TestMode {
  TestMode(bool file_type_filter, bool tablet_mode)
      : file_type_filter(file_type_filter), tablet_mode(tablet_mode) {}

  static testing::internal::ParamGenerator<TestMode> SystemWebAppValues() {
    return ::testing::Values(TestMode(false, false), TestMode(false, true),
                             TestMode(true, false), TestMode(true, true));
  }

  bool file_type_filter;
  bool tablet_mode;
};

// TODO(b/194969976): Print human readable test names instead TestName/1, etc.
class BaseSelectFileDialogExtensionBrowserTest
    : public extensions::ExtensionBrowserTest,
      public testing::WithParamInterface<TestMode> {
 public:
  BaseSelectFileDialogExtensionBrowserTest() {
    use_file_type_filter_ = GetParam().file_type_filter;
  }

  enum DialogButtonType {
    DIALOG_BTN_OK,
    DIALOG_BTN_CANCEL
  };

  void SetUp() override {
    // Create the dialog wrapper and listener objects.
    listener_ = std::make_unique<MockSelectFileDialogListener>();
    dialog_ = new SelectFileDialogExtension(listener_.get(), nullptr);

    // One mount point will be needed. Files app looks for the "Downloads"
    // volume mount point by default, so use that.
    base::FilePath tmp_path;
    base::PathService::Get(base::DIR_TEMP, &tmp_path);
    ASSERT_TRUE(tmp_dir_.CreateUniqueTempDirUnderPath(tmp_path));
    downloads_dir_ =
        tmp_dir_.GetPath().AppendASCII("My Files").AppendASCII("Downloads");
    base::CreateDirectory(downloads_dir_);

    // Must run after our setup because it actually runs the test.
    extensions::ExtensionBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    extensions::ExtensionBrowserTest::SetUpOnMainThread();
    CHECK(profile());

    // Create a file system mount point for the "Downloads" directory.
    EXPECT_TRUE(
        file_manager::VolumeManager::Get(profile())
            ->RegisterDownloadsDirectoryForTesting(downloads_dir_.DirName()));
    profile()->GetPrefs()->SetFilePath(prefs::kDownloadDefaultDirectory,
                                       downloads_dir_);

    // The test resources are setup: enable and add default ChromeOS component
    // extensions now and not before: crbug.com/831074, crbug.com/804413.
    file_manager::test::AddDefaultComponentExtensionsOnMainThread(profile());
  }

  void TearDown() override {
    extensions::ExtensionBrowserTest::TearDown();

    // Delete the dialogs first since they hold a pointer to their listener.
    dialog_.reset();
    listener_.reset();
    second_dialog_.reset();
    second_listener_.reset();
  }

  void CheckJavascriptErrors() {
    content::RenderFrameHost* host = dialog_->GetPrimaryMainFrame();
    ASSERT_EQ(0, content::EvalJs(host, "window.JSErrorCount"));
  }

  void ClickElement(const std::string& selector) {
    content::RenderFrameHost* frame_host = dialog_->GetPrimaryMainFrame();

    auto* web_contents = content::WebContents::FromRenderFrameHost(frame_host);
    CHECK(web_contents);

    int x = content::EvalJs(web_contents,
                            "var bounds = document.querySelector('" + selector +
                                "').getBoundingClientRect();"
                                "Math.floor(bounds.left + bounds.width / 2);")
                .ExtractInt();

    int y = content::EvalJs(web_contents,
                            "var bounds = document.querySelector('" + selector +
                                "').getBoundingClientRect();"
                                "Math.floor(bounds.top + bounds.height / 2);")
                .ExtractInt();

    LOG(INFO) << "ClickElement " << selector << " (" << x << "," << y << ")";
    constexpr auto kButton = blink::WebMouseEvent::Button::kLeft;
    content::SimulateMouseClickAt(web_contents, 0, kButton, gfx::Point(x, y));
  }

  void OpenDialog(ui::SelectFileDialog::Type dialog_type,
                  const base::FilePath& file_path,
                  const gfx::NativeWindow& owning_window,
                  const std::string& additional_message,
                  const GURL* caller = nullptr) {
    if (GetParam().tablet_mode) {
      ash::ShellTestApi().SetTabletModeEnabledForTest(true);
    }
    // Open the file dialog: Files app will signal that it is loaded via the
    // "ready" chrome.test.sendMessage().
    ExtensionTestMessageListener init_listener("ready");

    std::unique_ptr<ExtensionTestMessageListener> additional_listener;
    if (!additional_message.empty()) {
      additional_listener =
          std::make_unique<ExtensionTestMessageListener>(additional_message);
    }

    std::u16string title;
    // Include a file type filter. This triggers additional functionality within
    // the Files app.
    ui::SelectFileDialog::FileTypeInfo file_types{{FILE_PATH_LITERAL("html")}};
    const ui::SelectFileDialog::FileTypeInfo* file_types_ptr =
        UseFileTypeFilter() ? &file_types : nullptr;

    dialog_->SelectFile(dialog_type, title, file_path, file_types_ptr, 0,
                        FILE_PATH_LITERAL(""), owning_window, caller);
    LOG(INFO) << "Waiting for JavaScript ready message.";
    ASSERT_TRUE(init_listener.WaitUntilSatisfied());

    if (additional_listener.get()) {
      LOG(INFO) << "Waiting for JavaScript " << additional_message
                << " message.";
      ASSERT_TRUE(additional_listener->WaitUntilSatisfied());
    }

    // Dialog should be running now.
    ASSERT_TRUE(dialog_->IsRunning(owning_window));

    ASSERT_NO_FATAL_FAILURE(CheckJavascriptErrors());
  }

  bool OpenDialogIsResizable() const {
    return dialog_->IsResizeable();  // See crrev.com/600185.
  }

  void TryOpeningSecondDialog(const gfx::NativeWindow& owning_window) {
    second_listener_ = std::make_unique<MockSelectFileDialogListener>();
    second_dialog_ =
        new SelectFileDialogExtension(second_listener_.get(), nullptr);

    // The dialog type is not relevant for this test but is required: use the
    // open file dialog type.
    second_dialog_->SelectFile(
        ui::SelectFileDialog::SELECT_OPEN_FILE, std::u16string() /* title */,
        base::FilePath() /* default_path */, nullptr /* file_types */,
        0 /* file_type_index */, FILE_PATH_LITERAL("") /* default_extension */,
        owning_window);
  }

  void ClickJsButton(content::RenderFrameHost* frame_host,
                     DialogButtonType button_type) {
    std::string button_class =
        (button_type == DIALOG_BTN_OK) ? ".button-panel .ok" :
                                         ".button-panel .cancel";
    std::u16string script = base::ASCIIToUTF16(
        "console.log(\'Test JavaScript injected.\');"
        "document.querySelector(\'" +
        button_class + "\').click();");
    // The file selection handler code closes the dialog but does not return
    // control to JavaScript, so do not wait for the script return value.
    frame_host->ExecuteJavaScriptForTests(script, base::NullCallback(),
                                          content::ISOLATED_WORLD_ID_GLOBAL);
  }

  void CloseDialog(DialogButtonType button_type,
                   const gfx::NativeWindow& owning_window) {
    // Inject JavaScript into the dialog to click the dialog |button_type|.
    content::RenderFrameHost* frame_host = dialog_->GetPrimaryMainFrame();

    ClickJsButton(frame_host, button_type);

    // Instead, wait for Listener notification that the window has closed.
    LOG(INFO) << "Waiting for window close notification.";
    listener_->WaitForCalled();

    // Dialog no longer believes it is running.
    if (owning_window)
      ASSERT_FALSE(dialog_->IsRunning(owning_window));
  }

  bool UseFileTypeFilter() { return use_file_type_filter_; }

  base::ScopedTempDir tmp_dir_;
  base::FilePath downloads_dir_;

  std::unique_ptr<MockSelectFileDialogListener> listener_;
  scoped_refptr<SelectFileDialogExtension> dialog_;

  std::unique_ptr<MockSelectFileDialogListener> second_listener_;
  scoped_refptr<SelectFileDialogExtension> second_dialog_;

  bool use_file_type_filter_;
};

// Tests FileDialog with and without file filter.
class SelectFileDialogExtensionBrowserTest
    : public BaseSelectFileDialogExtensionBrowserTest {};

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest, CreateAndDestroy) {
  // The browser window must exist for us to test dialog's parent window.
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Before we call SelectFile, the dialog should not be running/visible.
  ASSERT_FALSE(dialog_->IsRunning(owning_window));
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest, DestroyListener) {
  // Some users of SelectFileDialog destroy their listener before cleaning
  // up the dialog.  Make sure we don't crash.
  dialog_->ListenerDestroyed();
  listener_.reset();
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest, DestroyListener2) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));

  // Get the Files app WebContents/Framehost, before deleting the dialog_.
  content::RenderFrameHost* frame_host = dialog_->GetPrimaryMainFrame();

  // Some users of SelectFileDialog destroy their listener before cleaning
  // up the dialog, delete the `dialog_`, however the
  // SystemFilesAppDialogDelegate will still be alive with the Files app
  // WebContents.  Make sure we don't crash.
  dialog_->ListenerDestroyed();
  listener_.reset();
  dialog_.reset();

  // This will close the FrameHost/WebContents and will try to close the
  // `dialog_`.
  ClickJsButton(frame_host, DIALOG_BTN_CANCEL);

  base::RunLoop().RunUntilIdle();
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest, CanResize) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));

  // The dialog should be resizable.
  ASSERT_EQ(!GetParam().tablet_mode, OpenDialogIsResizable());

  // Click the "Cancel" button. This closes the dialog thus removing it from
  // `PendingDialog::map_`. `PendingDialog::map_` otherwise prevents the dialog
  // from being destroyed on `reset()` in test TearDown and the
  // `SelectFileDialog::listener_` becomes dangling.
  CloseDialog(DIALOG_BTN_CANCEL, owning_window);
}


IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest,
                       SelectFileAndCancel) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));
  // Click the "Cancel" button.
  CloseDialog(DIALOG_BTN_CANCEL, owning_window);

  // Listener should have been informed of the cancellation.
  ASSERT_FALSE(listener_->file_selected());
  ASSERT_TRUE(listener_->canceled());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest,
                       SelectFileAndOpen) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Create an empty file to provide the file to open.
  const base::FilePath test_file =
      downloads_dir_.AppendASCII("file_manager_test.html");
  {
    base::ScopedAllowBlockingForTesting allow_io;
    FILE* fp = base::OpenFile(test_file, "w");
    ASSERT_TRUE(fp != nullptr);
    ASSERT_TRUE(base::CloseFile(fp));
  }

  // Open the file dialog, providing a path to the file to open so the dialog
  // will automatically select it.  Ensure the "Open" button is enabled by
  // waiting for notification from chrome.test.sendMessage().
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     test_file, owning_window, "dialog-ready"));
  // Click the "Open" button.
  CloseDialog(DIALOG_BTN_OK, owning_window);

  // Listener should have been informed that the file was opened.
  ASSERT_TRUE(listener_->file_selected());
  ASSERT_FALSE(listener_->canceled());
  ASSERT_EQ(test_file, listener_->path());
}

// TODO(crbug.com/40249076): Re-enable this test
IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest,
                       DISABLED_SelectFileAndSave) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog to save a file, providing a suggested file path.
  // Ensure the "Save" button is enabled by waiting for notification from
  // chrome.test.sendMessage().
  const base::FilePath test_file =
      downloads_dir_.AppendASCII("file_manager_save.html");
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE,
                                     test_file, owning_window, "dialog-ready"));
  // Click the "Save" button.
  CloseDialog(DIALOG_BTN_OK, owning_window);

  // Listener should have been informed that the file was saved.
  ASSERT_TRUE(listener_->file_selected());
  ASSERT_FALSE(listener_->canceled());
  ASSERT_EQ(test_file, listener_->path());
}

// TODO(crbug.com/40249076): Re-enable this test
IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest,
                       DISABLED_SelectFileVirtualKeyboard) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Enable the virtual keyboard.
  ash::ShellTestApi().EnableVirtualKeyboard();

  auto* client = ChromeKeyboardControllerClient::Get();
  EXPECT_FALSE(client->is_keyboard_visible());

  // Open the file dialog to save a file, providing a suggested file path.
  // Ensure the "Save" button is enabled by waiting for notification from
  // chrome.test.sendMessage().
  const base::FilePath test_file =
      downloads_dir_.AppendASCII("file_manager_save.html");
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE,
                                     test_file, owning_window, "dialog-ready"));

  // Click the dialog's filename input element.
  ASSERT_NO_FATAL_FAILURE(ClickElement("#filename-input-textbox"));

  // The virtual keyboard should be shown.
  KeyboardVisibleWaiter().Wait();
  EXPECT_TRUE(client->is_keyboard_visible());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest,
                       OpenSingletonTabAndCancel) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));

  // Open a singleton tab in background.
  NavigateParams p(browser(), GURL("http://www.google.com"),
                   ui::PAGE_TRANSITION_LINK);
  p.window_action = NavigateParams::SHOW_WINDOW;
  p.disposition = WindowOpenDisposition::SINGLETON_TAB;
  Navigate(&p);

  // Click the "Cancel" button.
  CloseDialog(DIALOG_BTN_CANCEL, owning_window);

  // Listener should have been informed of the cancellation.
  ASSERT_FALSE(listener_->file_selected());
  ASSERT_TRUE(listener_->canceled());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest, OpenTwoDialogs) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));

  // Requests to open a second file dialog should fail.
  TryOpeningSecondDialog(owning_window);
  ASSERT_FALSE(second_dialog_->IsRunning(owning_window));

  // Click the "Cancel" button.
  CloseDialog(DIALOG_BTN_CANCEL, owning_window);

  // Listener should have been informed of the cancellation.
  ASSERT_FALSE(listener_->file_selected());
  ASSERT_TRUE(listener_->canceled());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest, FileInputElement) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Start the embedded test server.
  base::FilePath source_dir;
  ASSERT_TRUE(
      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_dir));
  auto test_data_dir = source_dir.AppendASCII("chrome")
                           .AppendASCII("test")
                           .AppendASCII("data")
                           .AppendASCII("chromeos")
                           .AppendASCII("file_manager");
  embedded_test_server()->ServeFilesFromDirectory(test_data_dir);
  ASSERT_TRUE(embedded_test_server()->Start());

  // Navigate the browser to the file input element test page.
  const GURL url = embedded_test_server()->GetURL("/file_input/element.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  ASSERT_EQ(url, web_contents->GetLastCommittedURL());

  // Create a listener for the file dialog's "ready" message.
  ExtensionTestMessageListener listener("ready");

  // Click the file <input> element to open the file dialog.
  constexpr auto kButton = blink::WebMouseEvent::Button::kLeft;
  content::SimulateMouseClickAt(web_contents, 0, kButton, gfx::Point(0, 0));

  // Wait for file dialog's "ready" message.
  EXPECT_TRUE(listener.WaitUntilSatisfied());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest,
                       OpenDialogWithoutOwningWindow) {
  gfx::NativeWindow owning_window = gfx::NativeWindow();

  // Open the file dialog with no |owning_window|.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));

  // Click the "Cancel" button.
  CloseDialog(DIALOG_BTN_CANCEL, owning_window);

  // Listener should have been informed of the cancellation.
  ASSERT_TRUE(listener_->canceled());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest, MultipleOpenFile) {
  // No use-after-free when Browser::OpenFile is called multiple times.
  browser()->OpenFile();
  browser()->OpenFile();
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionBrowserTest,
                       DialogCallerSetWhenPassed) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  const std::string url = "https://example.com/";
  const GURL caller = GURL(url);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, "",
                                     &caller));

  // Check that the caller field is set correctly.
  ASSERT_TRUE(dialog_->owner_.dialog_caller.has_value());
  ASSERT_EQ(dialog_->owner_.dialog_caller->url().value(), url);

  // Click the "Cancel" button.
  CloseDialog(DIALOG_BTN_CANCEL, owning_window);

  // Listener should have been informed of the cancellation.
  ASSERT_FALSE(listener_->file_selected());
  ASSERT_TRUE(listener_->canceled());
}

INSTANTIATE_TEST_SUITE_P(SystemWebApp,
                         SelectFileDialogExtensionBrowserTest,
                         TestMode::SystemWebAppValues());

// Tests that ash window has correct colors for GM2.
// TODO(adanilo) factor out the unnecessary override of Setup().
class SelectFileDialogExtensionFlagTest
    : public BaseSelectFileDialogExtensionBrowserTest {
  void SetUp() override {
    BaseSelectFileDialogExtensionBrowserTest::SetUp();
  }
};

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionFlagTest,
                       DialogColoredTitle_Light) {
  ash::DarkLightModeController::Get()->SetDarkModeEnabledForTest(false);
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));
  content::RenderFrameHost* frame_host = dialog_->GetPrimaryMainFrame();
  aura::Window* dialog_window =
      frame_host->GetNativeView()->GetToplevelWindow();
  // This is cros_tokens::kDialogTitleBarColorLight
  SkColor dialog_title_bar_color = SkColorSetRGB(0xDF, 0xE0, 0xE1);
  EXPECT_EQ(dialog_window->GetProperty(chromeos::kFrameActiveColorKey),
            dialog_title_bar_color);
  EXPECT_EQ(dialog_window->GetProperty(chromeos::kFrameInactiveColorKey),
            dialog_title_bar_color);

  CloseDialog(DIALOG_BTN_CANCEL, owning_window);
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionFlagTest,
                       DialogColoredTitle_Dark) {
  ash::DarkLightModeController::Get()->SetDarkModeEnabledForTest(true);
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));
  content::RenderFrameHost* frame_host = dialog_->GetPrimaryMainFrame();
  aura::Window* dialog_window =
      frame_host->GetNativeView()->GetToplevelWindow();
  // This is cros_tokens::kDialogTitleBarColorDark
  SkColor dialog_title_bar_color = SkColorSetRGB(0x4D, 0x4D, 0x50);
  EXPECT_EQ(dialog_window->GetProperty(chromeos::kFrameActiveColorKey),
            dialog_title_bar_color);
  EXPECT_EQ(dialog_window->GetProperty(chromeos::kFrameInactiveColorKey),
            dialog_title_bar_color);

  CloseDialog(DIALOG_BTN_CANCEL, owning_window);
}

INSTANTIATE_TEST_SUITE_P(SystemWebApp,
                         SelectFileDialogExtensionFlagTest,
                         TestMode::SystemWebAppValues());

using SelectFileDialogExtensionDarkLightModeEnabledTest =
    BaseSelectFileDialogExtensionBrowserTest;

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionDarkLightModeEnabledTest,
                       ColorModeChange) {
  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Open the file dialog on the default path.
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     base::FilePath(), owning_window, ""));
  content::RenderFrameHost* frame_host = dialog_->GetPrimaryMainFrame();
  aura::Window* dialog_window =
      frame_host->GetNativeView()->GetToplevelWindow();

  auto* dark_light_mode_controller = ash::DarkLightModeController::Get();
  bool dark_mode_enabled = dark_light_mode_controller->IsDarkModeEnabled();
  SkColor initial_active_color =
      dialog_window->GetProperty(chromeos::kFrameActiveColorKey);
  SkColor initial_inactive_color =
      dialog_window->GetProperty(chromeos::kFrameInactiveColorKey);
  Profile* profile = chrome_test_utils::GetProfile(this);
  PrefService* prefs = profile->GetPrefs();

  // Switch the color mode.
  prefs->SetBoolean(ash::prefs::kDarkModeEnabled, !dark_mode_enabled);
  EXPECT_EQ(!dark_mode_enabled,
            dark_light_mode_controller->IsDarkModeEnabled());

  // Active and inactive colors in the other mode should be different from the
  // initial mode.
  EXPECT_NE(dialog_window->GetProperty(chromeos::kFrameActiveColorKey),
            initial_active_color);
  EXPECT_NE(dialog_window->GetProperty(chromeos::kFrameInactiveColorKey),
            initial_inactive_color);

  CloseDialog(DIALOG_BTN_CANCEL, owning_window);
}

INSTANTIATE_TEST_SUITE_P(SystemWebApp,
                         SelectFileDialogExtensionDarkLightModeEnabledTest,
                         TestMode::SystemWebAppValues());

// Tests applying policies before notifying listeners.
class SelectFileDialogExtensionPolicyTest
    : public BaseSelectFileDialogExtensionBrowserTest {
 protected:
  class MockFilesController : public policy::DlpFilesControllerAsh {
   public:
    explicit MockFilesController(const policy::DlpRulesManager& rules_manager,
                                 Profile* profile)
        : DlpFilesControllerAsh(rules_manager, profile) {}
    ~MockFilesController() override = default;

    MOCK_METHOD(void,
                CheckIfDownloadAllowed,
                (const policy::DlpFileDestination&,
                 const base::FilePath&,
                 base::OnceCallback<void(bool)>),
                (override));
    MOCK_METHOD(void,
                FilterDisallowedUploads,
                (std::vector<ui::SelectedFileInfo>,
                 const policy::DlpFileDestination&,
                 base::OnceCallback<void(std::vector<ui::SelectedFileInfo>)>),
                (override));
  };

  void TearDownOnMainThread() override {
    // Make sure the rules manager does not return a freed files controller.
    ON_CALL(*rules_manager_, GetDlpFilesController)
        .WillByDefault(testing::Return(nullptr));

    // The files controller must be destroyed before the profile since it's
    // holding a pointer to it.
    mock_files_controller_.reset();
    BaseSelectFileDialogExtensionBrowserTest::TearDownOnMainThread();
  }

  std::unique_ptr<KeyedService> SetDlpRulesManager(
      content::BrowserContext* context) {
    auto dlp_rules_manager =
        std::make_unique<testing::NiceMock<policy::MockDlpRulesManager>>(
            Profile::FromBrowserContext(context));
    rules_manager_ = dlp_rules_manager.get();

    mock_files_controller_ = std::make_unique<MockFilesController>(
        *rules_manager_, Profile::FromBrowserContext(context));
    ON_CALL(*rules_manager_, GetDlpFilesController)
        .WillByDefault(testing::Return(mock_files_controller_.get()));

    return dlp_rules_manager;
  }

  void SetupRulesManager() {
    policy::DlpRulesManagerFactory::GetInstance()->SetTestingFactory(
        profile(), base::BindRepeating(
                       &SelectFileDialogExtensionPolicyTest::SetDlpRulesManager,
                       base::Unretained(this)));
    ASSERT_TRUE(policy::DlpRulesManagerFactory::GetForPrimaryProfile());

    ON_CALL(*rules_manager_, IsFilesPolicyEnabled)
        .WillByDefault(testing::Return(true));
  }

  raw_ptr<policy::MockDlpRulesManager, DanglingUntriaged> rules_manager_ =
      nullptr;
  std::unique_ptr<MockFilesController> mock_files_controller_ = nullptr;
  raw_ptr<storage::ExternalMountPoints> mount_points_ = nullptr;
};

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionPolicyTest, DlpDownloadAllow) {
  SetupRulesManager();

  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  const std::string url = "https://example.com/";
  const GURL caller = GURL(url);

  // Open the file dialog to save a file, providing a suggested file path.
  // Ensure the "Save" button is enabled by waiting for notification from
  // chrome.test.sendMessage().
  const base::FilePath test_file =
      downloads_dir_.AppendASCII("file_manager_save.html");
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE,
                                     test_file, owning_window, "dialog-ready",
                                     &caller));

  EXPECT_CALL(
      *mock_files_controller_.get(),
      CheckIfDownloadAllowed(policy::DlpFileDestination(caller), test_file,
                             base::test::IsNotNullCallback()))
      .WillOnce(base::test::RunOnceCallback<2>(true));

  // Click the "Save" button.
  CloseDialog(DIALOG_BTN_OK, owning_window);

  // Listener should have been informed of the selection.
  ASSERT_TRUE(listener_->file_selected());
  ASSERT_FALSE(listener_->canceled());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionPolicyTest, DlpDownloadBlock) {
  SetupRulesManager();

  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  const std::string url = "https://example.com/";
  const GURL caller = GURL(url);

  // Open the file dialog to save a file, providing a suggested file path.
  // Ensure the "Save" button is enabled by waiting for notification from
  // chrome.test.sendMessage().
  const base::FilePath test_file =
      downloads_dir_.AppendASCII("file_manager_save.html");
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE,
                                     test_file, owning_window, "dialog-ready",
                                     &caller));

  EXPECT_CALL(
      *mock_files_controller_.get(),
      CheckIfDownloadAllowed(policy::DlpFileDestination(caller), test_file,
                             base::test::IsNotNullCallback()))
      .WillOnce(base::test::RunOnceCallback<2>(false));

  // Click the "Save" button.
  CloseDialog(DIALOG_BTN_OK, owning_window);

  // Listener should have been informed of the cancellation.
  ASSERT_FALSE(listener_->file_selected());
  ASSERT_TRUE(listener_->canceled());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionPolicyTest, DlpUploadAllow) {
  SetupRulesManager();

  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Create an empty file to provide the file to open.
  const base::FilePath test_file =
      downloads_dir_.AppendASCII("file_manager_test.html");
  {
    base::ScopedAllowBlockingForTesting allow_io;
    FILE* fp = base::OpenFile(test_file, "w");
    ASSERT_TRUE(fp != nullptr);
    ASSERT_TRUE(base::CloseFile(fp));
  }
  base::FilePath test_file_virtual_path;
  ASSERT_TRUE(storage::ExternalMountPoints::GetSystemInstance()->GetVirtualPath(
      test_file, &test_file_virtual_path));

  const std::string url = "https://example.com/";
  const GURL caller = GURL(url);

  // Open the file dialog, providing a path to the file to open so the dialog
  // will automatically select it.  Ensure the "Open" button is enabled by
  // waiting for notification from chrome.test.sendMessage().
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     test_file, owning_window, "dialog-ready",
                                     &caller));

  std::vector<ui::SelectedFileInfo> selected_files;
  auto selected_file = ui::SelectedFileInfo(test_file);
  selected_file.virtual_path = test_file_virtual_path;
  selected_files.push_back(std::move(selected_file));
  EXPECT_CALL(*mock_files_controller_.get(),
              FilterDisallowedUploads(selected_files,
                                      policy::DlpFileDestination(caller),
                                      base::test::IsNotNullCallback()))
      .WillOnce(base::test::RunOnceCallback<2>(selected_files));

  // Click the "Save" button.
  CloseDialog(DIALOG_BTN_OK, owning_window);

  // Listener should have been informed of the selection.
  ASSERT_TRUE(listener_->file_selected());
  ASSERT_FALSE(listener_->canceled());
}

IN_PROC_BROWSER_TEST_P(SelectFileDialogExtensionPolicyTest, DlpUploadBlock) {
  SetupRulesManager();

  gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
  ASSERT_NE(nullptr, owning_window);

  // Create an empty file to provide the file to open.
  const base::FilePath test_file =
      downloads_dir_.AppendASCII("file_manager_test.html");
  {
    base::ScopedAllowBlockingForTesting allow_io;
    FILE* fp = base::OpenFile(test_file, "w");
    ASSERT_TRUE(fp != nullptr);
    ASSERT_TRUE(base::CloseFile(fp));
  }
  base::FilePath test_file_virtual_path;
  ASSERT_TRUE(storage::ExternalMountPoints::GetSystemInstance()->GetVirtualPath(
      test_file, &test_file_virtual_path));

  const std::string url = "https://example.com/";
  const GURL caller = GURL(url);

  // Open the file dialog, providing a path to the file to open so the dialog
  // will automatically select it.  Ensure the "Open" button is enabled by
  // waiting for notification from chrome.test.sendMessage().
  ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
                                     test_file, owning_window, "dialog-ready",
                                     &caller));

  std::vector<ui::SelectedFileInfo> selected_files;
  auto selected_file = ui::SelectedFileInfo(test_file);
  selected_file.virtual_path = test_file_virtual_path;
  selected_files.push_back(std::move(selected_file));
  EXPECT_CALL(*mock_files_controller_.get(),
              FilterDisallowedUploads(std::move(selected_files),
                                      policy::DlpFileDestination(caller),
                                      base::test::IsNotNullCallback()))
      .WillOnce(
          base::test::RunOnceCallback<2>(std::vector<ui::SelectedFileInfo>()));

  // Click the "Save" button.
  CloseDialog(DIALOG_BTN_OK, owning_window);

  // Listener should have been informed of the cancellation.
  ASSERT_FALSE(listener_->file_selected());
  ASSERT_TRUE(listener_->canceled());
}

INSTANTIATE_TEST_SUITE_P(SystemWebApp,
                         SelectFileDialogExtensionPolicyTest,
                         TestMode::SystemWebAppValues());