| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // 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 "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/logging.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/path_service.h" |
| #include "base/prefs/pref_service.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/platform_thread.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/chromeos/file_manager/volume_manager.h" |
| #include "chrome/browser/extensions/component_loader.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/pref_names.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/browser/notification_types.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/test/test_utils.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "ui/shell_dialogs/select_file_dialog.h" |
| #include "ui/shell_dialogs/selected_file_info.h" |
| |
| using content::BrowserContext; |
| |
| // Mock listener used by test below. |
| class MockSelectFileDialogListener : public ui::SelectFileDialog::Listener { |
| public: |
| MockSelectFileDialogListener() |
| : file_selected_(false), |
| canceled_(false), |
| params_(NULL) { |
| } |
| |
| bool file_selected() const { return file_selected_; } |
| bool canceled() const { return canceled_; } |
| base::FilePath path() const { return path_; } |
| void* params() const { return params_; } |
| |
| // ui::SelectFileDialog::Listener implementation. |
| virtual void FileSelected(const base::FilePath& path, |
| int index, |
| void* params) override { |
| file_selected_ = true; |
| path_ = path; |
| params_ = params; |
| } |
| virtual void FileSelectedWithExtraInfo( |
| const ui::SelectedFileInfo& selected_file_info, |
| int index, |
| void* params) override { |
| FileSelected(selected_file_info.local_path, index, params); |
| } |
| virtual void MultiFilesSelected( |
| const std::vector<base::FilePath>& files, void* params) override {} |
| virtual void FileSelectionCanceled(void* params) override { |
| canceled_ = true; |
| params_ = params; |
| } |
| |
| private: |
| bool file_selected_; |
| bool canceled_; |
| base::FilePath path_; |
| void* params_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MockSelectFileDialogListener); |
| }; |
| |
| class SelectFileDialogExtensionBrowserTest : public ExtensionBrowserTest { |
| public: |
| enum DialogButtonType { |
| DIALOG_BTN_OK, |
| DIALOG_BTN_CANCEL |
| }; |
| |
| virtual void SetUp() override { |
| extensions::ComponentLoader::EnableBackgroundExtensionsForTesting(); |
| |
| // Create the dialog wrapper object, but don't show it yet. |
| listener_.reset(new MockSelectFileDialogListener()); |
| dialog_ = new SelectFileDialogExtension(listener_.get(), NULL); |
| |
| // We have to provide at least one mount point. |
| // File manager looks for "Downloads" mount point, so use this name. |
| base::FilePath tmp_path; |
| PathService::Get(base::DIR_TEMP, &tmp_path); |
| ASSERT_TRUE(tmp_dir_.CreateUniqueTempDirUnderPath(tmp_path)); |
| downloads_dir_ = tmp_dir_.path().Append("Downloads"); |
| base::CreateDirectory(downloads_dir_); |
| |
| // Must run after our setup because it actually runs the test. |
| ExtensionBrowserTest::SetUp(); |
| } |
| |
| virtual void TearDown() override { |
| ExtensionBrowserTest::TearDown(); |
| |
| // Delete the dialog first, as it holds a pointer to the listener. |
| dialog_ = NULL; |
| listener_.reset(); |
| |
| second_dialog_ = NULL; |
| second_listener_.reset(); |
| } |
| |
| // Creates a file system mount point for a directory. |
| void AddMountPoint(const base::FilePath& path) { |
| EXPECT_TRUE(file_manager::VolumeManager::Get( |
| browser()->profile())->RegisterDownloadsDirectoryForTesting(path)); |
| browser()->profile()->GetPrefs()->SetFilePath( |
| prefs::kDownloadDefaultDirectory, downloads_dir_); |
| } |
| |
| void CheckJavascriptErrors() { |
| content::RenderFrameHost* host = |
| dialog_->GetRenderViewHost()->GetMainFrame(); |
| scoped_ptr<base::Value> value = |
| content::ExecuteScriptAndGetValue(host, "window.JSErrorCount"); |
| int js_error_count = 0; |
| ASSERT_TRUE(value->GetAsInteger(&js_error_count)); |
| ASSERT_EQ(0, js_error_count); |
| } |
| |
| void OpenDialog(ui::SelectFileDialog::Type dialog_type, |
| const base::FilePath& file_path, |
| const gfx::NativeWindow& owning_window, |
| const std::string& additional_message) { |
| // Spawn a dialog to open a file. The dialog will signal that it is ready |
| // via chrome.test.sendMessage() in the extension JavaScript. |
| ExtensionTestMessageListener init_listener("ready", false /* will_reply */); |
| |
| scoped_ptr<ExtensionTestMessageListener> additional_listener; |
| if (!additional_message.empty()) { |
| additional_listener.reset( |
| new ExtensionTestMessageListener(additional_message, false)); |
| } |
| |
| dialog_->SelectFile(dialog_type, |
| base::string16() /* title */, |
| file_path, |
| NULL /* file_types */, |
| 0 /* file_type_index */, |
| FILE_PATH_LITERAL("") /* default_extension */, |
| owning_window, |
| this /* params */); |
| |
| 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()); |
| } |
| |
| void TryOpeningSecondDialog(const gfx::NativeWindow& owning_window) { |
| second_listener_.reset(new MockSelectFileDialogListener()); |
| second_dialog_ = new SelectFileDialogExtension(second_listener_.get(), |
| NULL); |
| |
| // At the moment we don't really care about dialog type, but we have to put |
| // some dialog type. |
| second_dialog_->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE, |
| base::string16() /* title */, |
| base::FilePath() /* default_path */, |
| NULL /* file_types */, |
| 0 /* file_type_index */, |
| FILE_PATH_LITERAL("") /* default_extension */, |
| owning_window, |
| this /* params */); |
| } |
| |
| void CloseDialog(DialogButtonType button_type, |
| const gfx::NativeWindow& owning_window) { |
| // Inject JavaScript to click the cancel button and wait for notification |
| // that the window has closed. |
| content::WindowedNotificationObserver host_destroyed( |
| content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, |
| content::NotificationService::AllSources()); |
| content::RenderViewHost* host = dialog_->GetRenderViewHost(); |
| std::string button_class = |
| (button_type == DIALOG_BTN_OK) ? ".button-panel .ok" : |
| ".button-panel .cancel"; |
| base::string16 script = base::ASCIIToUTF16( |
| "console.log(\'Test JavaScript injected.\');" |
| "document.querySelector(\'" + button_class + "\').click();"); |
| // The file selection handler closes the dialog and does not return control |
| // to JavaScript, so do not wait for return values. |
| host->GetMainFrame()->ExecuteJavaScript(script); |
| LOG(INFO) << "Waiting for window close notification."; |
| host_destroyed.Wait(); |
| |
| // Dialog no longer believes it is running. |
| ASSERT_FALSE(dialog_->IsRunning(owning_window)); |
| } |
| |
| scoped_ptr<MockSelectFileDialogListener> listener_; |
| scoped_refptr<SelectFileDialogExtension> dialog_; |
| |
| scoped_ptr<MockSelectFileDialogListener> second_listener_; |
| scoped_refptr<SelectFileDialogExtension> second_dialog_; |
| |
| base::ScopedTempDir tmp_dir_; |
| base::FilePath downloads_dir_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, CreateAndDestroy) { |
| // Browser window must be up for us to test dialog window parent. |
| gfx::NativeWindow native_window = browser()->window()->GetNativeWindow(); |
| ASSERT_TRUE(native_window != NULL); |
| |
| // Before we call SelectFile, dialog is not running/visible. |
| ASSERT_FALSE(dialog_->IsRunning(native_window)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, DestroyListener) { |
| // Some users of SelectFileDialog destroy their listener before cleaning |
| // up the dialog. Make sure we don't crash. |
| dialog_->ListenerDestroyed(); |
| listener_.reset(); |
| } |
| |
| // TODO(jamescook): Add a test for selecting a file for an <input type='file'/> |
| // page element, as that uses different memory management pathways. |
| // crbug.com/98791 |
| |
| IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, |
| SelectFileAndCancel) { |
| AddMountPoint(downloads_dir_); |
| |
| gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); |
| |
| // base::FilePath() for default path. |
| ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, |
| base::FilePath(), owning_window, "")); |
| |
| // Press 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()); |
| ASSERT_EQ(this, listener_->params()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, |
| SelectFileAndOpen) { |
| AddMountPoint(downloads_dir_); |
| |
| base::FilePath test_file = |
| downloads_dir_.AppendASCII("file_manager_test.html"); |
| |
| // Create an empty file to give us something to select. |
| FILE* fp = base::OpenFile(test_file, "w"); |
| ASSERT_TRUE(fp != NULL); |
| ASSERT_TRUE(base::CloseFile(fp)); |
| |
| gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); |
| |
| // Spawn a dialog to open a file. Provide the path to the file so the dialog |
| // will automatically select it. Ensure that the OK button is enabled by |
| // waiting for chrome.test.sendMessage('selection-change-complete'). |
| ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, |
| test_file, owning_window, |
| "selection-change-complete")); |
| |
| // Click 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()); |
| ASSERT_EQ(this, listener_->params()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, |
| SelectFileAndSave) { |
| AddMountPoint(downloads_dir_); |
| |
| base::FilePath test_file = |
| downloads_dir_.AppendASCII("file_manager_test.html"); |
| |
| gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); |
| |
| // Spawn a dialog to save a file, providing a suggested path. |
| // Ensure "Save" button is enabled by waiting for notification from |
| // chrome.test.sendMessage(). |
| ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE, |
| test_file, owning_window, |
| "directory-change-complete")); |
| |
| // Click save button. |
| CloseDialog(DIALOG_BTN_OK, owning_window); |
| |
| // Listener should have been informed that the file was selected. |
| ASSERT_TRUE(listener_->file_selected()); |
| ASSERT_FALSE(listener_->canceled()); |
| ASSERT_EQ(test_file, listener_->path()); |
| ASSERT_EQ(this, listener_->params()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, |
| OpenSingletonTabAndCancel) { |
| AddMountPoint(downloads_dir_); |
| |
| gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); |
| |
| ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, |
| base::FilePath(), owning_window, "")); |
| |
| // Open a singleton tab in background. |
| chrome::NavigateParams p(browser(), GURL("http://www.google.com"), |
| ui::PAGE_TRANSITION_LINK); |
| p.window_action = chrome::NavigateParams::SHOW_WINDOW; |
| p.disposition = SINGLETON_TAB; |
| chrome::Navigate(&p); |
| |
| // Press 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()); |
| ASSERT_EQ(this, listener_->params()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, |
| OpenTwoDialogs) { |
| AddMountPoint(downloads_dir_); |
| |
| gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); |
| |
| ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, |
| base::FilePath(), owning_window, "")); |
| |
| TryOpeningSecondDialog(owning_window); |
| |
| // Second dialog should not be running. |
| ASSERT_FALSE(second_dialog_->IsRunning(owning_window)); |
| |
| // Click 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()); |
| ASSERT_EQ(this, listener_->params()); |
| } |