// 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/file_util.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/path_service.h" #include "base/scoped_temp_dir.h" #include "base/threading/platform_thread.h" #include "base/utf_string_conversions.h" // ASCIIToUTF16 #include "build/build_config.h" #include "chrome/browser/extensions/extension_browsertest.h" #include "chrome/browser/extensions/extension_test_message_listener.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/browser/ui/select_file_dialog.h" #include "chrome/common/chrome_paths.h" #include "chrome/test/base/ui_test_utils.h" #include "content/browser/renderer_host/render_view_host.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_types.h" #include "webkit/fileapi/file_system_context.h" #include "webkit/fileapi/file_system_mount_point_provider.h" // Mock listener used by test below. class MockSelectFileDialogListener : public SelectFileDialog::Listener { public: MockSelectFileDialogListener() : file_selected_(false), canceled_(false), params_(NULL) { } bool file_selected() const { return file_selected_; } bool canceled() const { return canceled_; } FilePath path() const { return path_; } void* params() const { return params_; } // SelectFileDialog::Listener implementation. virtual void FileSelected(const FilePath& path, int index, void* params) { file_selected_ = true; path_ = path; params_ = params; } virtual void MultiFilesSelected( const std::vector& files, void* params) {} virtual void FileSelectionCanceled(void* params) { canceled_ = true; params_ = params; } private: bool file_selected_; bool canceled_; 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 { // Create the dialog wrapper object, but don't show it yet. listener_.reset(new MockSelectFileDialogListener()); dialog_ = new SelectFileDialogExtension(listener_.get()); // We have to provide at least one mount point. // File manager looks for "Downloads" mount point, so use this name. FilePath tmp_path; PathService::Get(base::DIR_TEMP, &tmp_path); ASSERT_TRUE(tmp_dir_.CreateUniqueTempDirUnderPath(tmp_path)); downloads_dir_ = tmp_dir_.path().Append("Downloads"); file_util::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 FilePath& path) { fileapi::ExternalFileSystemMountPointProvider* provider = browser()->profile()->GetFileSystemContext()->external_provider(); provider->AddMountPoint(path); } void OpenDialog(SelectFileDialog::Type dialog_type, const 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("worker-initialized", false /* will_reply */); scoped_ptr additional_listener; if (!additional_message.empty()) { additional_listener.reset( new ExtensionTestMessageListener(additional_message, false)); } dialog_->SelectFile(dialog_type, string16() /* title */, file_path, NULL /* file_types */, 0 /* file_type_index */, FILE_PATH_LITERAL("") /* default_extension */, NULL /* source_contents */, 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)); } void TryOpeningSecondDialog(const gfx::NativeWindow& owning_window) { second_listener_.reset(new MockSelectFileDialogListener()); second_dialog_ = new SelectFileDialogExtension(second_listener_.get()); // At the moment we don't really care about dialog type, but we have to put // some dialog type. second_dialog_->SelectFile(SelectFileDialog::SELECT_OPEN_FILE, string16() /* title */, FilePath() /* default_path */, NULL /* file_types */, 0 /* file_type_index */, FILE_PATH_LITERAL("") /* default_extension */, NULL /* source_contents */, 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. ui_test_utils::WindowedNotificationObserver host_destroyed( content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, content::NotificationService::AllSources()); RenderViewHost* host = dialog_->GetRenderViewHost(); string16 main_frame; std::string button_class = (button_type == DIALOG_BTN_OK) ? ".ok" : ".cancel"; string16 script = 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->ExecuteJavascriptInWebFrame(main_frame, 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 listener_; scoped_refptr dialog_; scoped_ptr second_listener_; scoped_refptr second_dialog_; ScopedTempDir tmp_dir_; 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()->GetNativeHandle(); 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 // page element, as that uses different memory management pathways. // crbug.com/98791 // TODO(jamescook): Make dialog work on non-ChromeOS platforms. crbug.com/97424 #if !defined(OS_CHROMEOS) #define MAYBE_SelectFileAndCancel DISABLED_SelectFileAndCancel #else #define MAYBE_SelectFileAndCancel SelectFileAndCancel #endif IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, MAYBE_SelectFileAndCancel) { AddMountPoint(downloads_dir_); gfx::NativeWindow owning_window = browser()->window()->GetNativeHandle(); // FilePath() for default path. OpenDialog(SelectFileDialog::SELECT_OPEN_FILE, 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()); } // TODO(jamescook): Make dialog work on non-ChromeOS platforms. crbug.com/97424 #if !defined(OS_CHROMEOS) #define MAYBE_SelectFileAndOpen DISABLED_SelectFileAndOpen #else #define MAYBE_SelectFileAndOpen SelectFileAndOpen #endif IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, MAYBE_SelectFileAndOpen) { AddMountPoint(downloads_dir_); FilePath test_file = downloads_dir_.AppendASCII("file_manager_test.html"); // Create an empty file to give us something to select. FILE* fp = file_util::OpenFile(test_file, "w"); ASSERT_TRUE(fp != NULL); ASSERT_TRUE(file_util::CloseFile(fp)); gfx::NativeWindow owning_window = browser()->window()->GetNativeHandle(); // 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'). // The extension starts a Web Worker to read file metadata, so it may send // 'selection-change-complete' before 'worker-initialized'. This is OK. OpenDialog(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()); } // TODO(jamescook): Make dialog work on non-ChromeOS platforms. crbug.com/97424 #if !defined(OS_CHROMEOS) #define MAYBE_SelectFileAndSave DISABLED_SelectFileAndSave #else #define MAYBE_SelectFileAndSave SelectFileAndSave #endif IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, MAYBE_SelectFileAndSave) { AddMountPoint(downloads_dir_); FilePath test_file = downloads_dir_.AppendASCII("file_manager_test.html"); gfx::NativeWindow owning_window = browser()->window()->GetNativeHandle(); // Spawn a dialog to save a file, providing a suggested path. // Ensure "Save" button is enabled by waiting for notification from // chrome.test.sendMessage(). // The extension starts a Web Worker to read file metadata, so it may send // 'directory-change-complete' before 'worker-initialized'. This is OK. OpenDialog(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()); } // TODO(jamescook): Make dialog work on non-ChromeOS platforms. crbug.com/97424 #if !defined(OS_CHROMEOS) #define MAYBE_OpenSingletonTabAndCancel DISABLED_OpenSingletonTabAndCancel #else #define MAYBE_OpenSingletonTabAndCancel OpenSingletonTabAndCancel #endif IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, MAYBE_OpenSingletonTabAndCancel) { AddMountPoint(downloads_dir_); gfx::NativeWindow owning_window = browser()->window()->GetNativeHandle(); OpenDialog(SelectFileDialog::SELECT_OPEN_FILE, FilePath(), owning_window, ""); // Open a singleton tab in background. browser::NavigateParams p(browser(), GURL("www.google.com"), content::PAGE_TRANSITION_LINK); p.window_action = browser::NavigateParams::SHOW_WINDOW; p.disposition = SINGLETON_TAB; browser::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()); } // TODO(jamescook): Make dialog work on non-ChromeOS platforms. crbug.com/97424 #if !defined(OS_CHROMEOS) #define MAYBE_OpenTwoDialogs DISABLED_OpenTwoDialogs #else #define MAYBE_OpenTwoDialogs OpenTwoDialogs #endif IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, MAYBE_OpenTwoDialogs) { AddMountPoint(downloads_dir_); gfx::NativeWindow owning_window = browser()->window()->GetNativeHandle(); OpenDialog(SelectFileDialog::SELECT_OPEN_FILE, 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()); }