1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/ui/views/select_file_dialog_extension.h" 6 7 #include "base/file_util.h" 8 #include "base/files/scoped_temp_dir.h" 9 #include "base/logging.h" 10 #include "base/memory/scoped_ptr.h" 11 #include "base/path_service.h" 12 #include "base/prefs/pref_service.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "base/threading/platform_thread.h" 15 #include "build/build_config.h" 16 #include "chrome/browser/chromeos/file_manager/volume_manager.h" 17 #include "chrome/browser/extensions/component_loader.h" 18 #include "chrome/browser/extensions/extension_browsertest.h" 19 #include "chrome/browser/extensions/extension_test_message_listener.h" 20 #include "chrome/browser/profiles/profile.h" 21 #include "chrome/browser/ui/browser.h" 22 #include "chrome/browser/ui/browser_navigator.h" 23 #include "chrome/browser/ui/browser_window.h" 24 #include "chrome/common/chrome_paths.h" 25 #include "chrome/common/pref_names.h" 26 #include "content/public/browser/notification_service.h" 27 #include "content/public/browser/notification_types.h" 28 #include "content/public/browser/render_frame_host.h" 29 #include "content/public/browser/render_view_host.h" 30 #include "content/public/test/test_utils.h" 31 #include "ui/shell_dialogs/select_file_dialog.h" 32 #include "ui/shell_dialogs/selected_file_info.h" 33 34 using content::BrowserContext; 35 36 // Mock listener used by test below. 37 class MockSelectFileDialogListener : public ui::SelectFileDialog::Listener { 38 public: 39 MockSelectFileDialogListener() 40 : file_selected_(false), 41 canceled_(false), 42 params_(NULL) { 43 } 44 45 bool file_selected() const { return file_selected_; } 46 bool canceled() const { return canceled_; } 47 base::FilePath path() const { return path_; } 48 void* params() const { return params_; } 49 50 // ui::SelectFileDialog::Listener implementation. 51 virtual void FileSelected(const base::FilePath& path, 52 int index, 53 void* params) OVERRIDE { 54 file_selected_ = true; 55 path_ = path; 56 params_ = params; 57 } 58 virtual void FileSelectedWithExtraInfo( 59 const ui::SelectedFileInfo& selected_file_info, 60 int index, 61 void* params) OVERRIDE { 62 FileSelected(selected_file_info.local_path, index, params); 63 } 64 virtual void MultiFilesSelected( 65 const std::vector<base::FilePath>& files, void* params) OVERRIDE {} 66 virtual void FileSelectionCanceled(void* params) OVERRIDE { 67 canceled_ = true; 68 params_ = params; 69 } 70 71 private: 72 bool file_selected_; 73 bool canceled_; 74 base::FilePath path_; 75 void* params_; 76 77 DISALLOW_COPY_AND_ASSIGN(MockSelectFileDialogListener); 78 }; 79 80 class SelectFileDialogExtensionBrowserTest : public ExtensionBrowserTest { 81 public: 82 enum DialogButtonType { 83 DIALOG_BTN_OK, 84 DIALOG_BTN_CANCEL 85 }; 86 87 virtual void SetUp() OVERRIDE { 88 extensions::ComponentLoader::EnableBackgroundExtensionsForTesting(); 89 90 // Create the dialog wrapper object, but don't show it yet. 91 listener_.reset(new MockSelectFileDialogListener()); 92 dialog_ = new SelectFileDialogExtension(listener_.get(), NULL); 93 94 // We have to provide at least one mount point. 95 // File manager looks for "Downloads" mount point, so use this name. 96 base::FilePath tmp_path; 97 PathService::Get(base::DIR_TEMP, &tmp_path); 98 ASSERT_TRUE(tmp_dir_.CreateUniqueTempDirUnderPath(tmp_path)); 99 downloads_dir_ = tmp_dir_.path().Append("Downloads"); 100 base::CreateDirectory(downloads_dir_); 101 102 // Must run after our setup because it actually runs the test. 103 ExtensionBrowserTest::SetUp(); 104 } 105 106 virtual void TearDown() OVERRIDE { 107 ExtensionBrowserTest::TearDown(); 108 109 // Delete the dialog first, as it holds a pointer to the listener. 110 dialog_ = NULL; 111 listener_.reset(); 112 113 second_dialog_ = NULL; 114 second_listener_.reset(); 115 } 116 117 // Creates a file system mount point for a directory. 118 void AddMountPoint(const base::FilePath& path) { 119 EXPECT_TRUE(file_manager::VolumeManager::Get( 120 browser()->profile())->RegisterDownloadsDirectoryForTesting(path)); 121 browser()->profile()->GetPrefs()->SetFilePath( 122 prefs::kDownloadDefaultDirectory, downloads_dir_); 123 } 124 125 void CheckJavascriptErrors() { 126 content::RenderFrameHost* host = 127 dialog_->GetRenderViewHost()->GetMainFrame(); 128 scoped_ptr<base::Value> value = 129 content::ExecuteScriptAndGetValue(host, "window.JSErrorCount"); 130 int js_error_count = 0; 131 ASSERT_TRUE(value->GetAsInteger(&js_error_count)); 132 ASSERT_EQ(0, js_error_count); 133 } 134 135 void OpenDialog(ui::SelectFileDialog::Type dialog_type, 136 const base::FilePath& file_path, 137 const gfx::NativeWindow& owning_window, 138 const std::string& additional_message) { 139 // Spawn a dialog to open a file. The dialog will signal that it is ready 140 // via chrome.test.sendMessage() in the extension JavaScript. 141 ExtensionTestMessageListener init_listener("worker-initialized", 142 false /* will_reply */); 143 144 scoped_ptr<ExtensionTestMessageListener> additional_listener; 145 if (!additional_message.empty()) { 146 additional_listener.reset( 147 new ExtensionTestMessageListener(additional_message, false)); 148 } 149 150 dialog_->SelectFile(dialog_type, 151 base::string16() /* title */, 152 file_path, 153 NULL /* file_types */, 154 0 /* file_type_index */, 155 FILE_PATH_LITERAL("") /* default_extension */, 156 owning_window, 157 this /* params */); 158 159 LOG(INFO) << "Waiting for JavaScript ready message."; 160 ASSERT_TRUE(init_listener.WaitUntilSatisfied()); 161 162 if (additional_listener.get()) { 163 LOG(INFO) << "Waiting for JavaScript " << additional_message 164 << " message."; 165 ASSERT_TRUE(additional_listener->WaitUntilSatisfied()); 166 } 167 168 // Dialog should be running now. 169 ASSERT_TRUE(dialog_->IsRunning(owning_window)); 170 171 ASSERT_NO_FATAL_FAILURE(CheckJavascriptErrors()); 172 } 173 174 void TryOpeningSecondDialog(const gfx::NativeWindow& owning_window) { 175 second_listener_.reset(new MockSelectFileDialogListener()); 176 second_dialog_ = new SelectFileDialogExtension(second_listener_.get(), 177 NULL); 178 179 // At the moment we don't really care about dialog type, but we have to put 180 // some dialog type. 181 second_dialog_->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE, 182 base::string16() /* title */, 183 base::FilePath() /* default_path */, 184 NULL /* file_types */, 185 0 /* file_type_index */, 186 FILE_PATH_LITERAL("") /* default_extension */, 187 owning_window, 188 this /* params */); 189 } 190 191 void CloseDialog(DialogButtonType button_type, 192 const gfx::NativeWindow& owning_window) { 193 // Inject JavaScript to click the cancel button and wait for notification 194 // that the window has closed. 195 content::WindowedNotificationObserver host_destroyed( 196 content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED, 197 content::NotificationService::AllSources()); 198 content::RenderViewHost* host = dialog_->GetRenderViewHost(); 199 std::string button_class = 200 (button_type == DIALOG_BTN_OK) ? ".button-panel .ok" : 201 ".button-panel .cancel"; 202 base::string16 script = base::ASCIIToUTF16( 203 "console.log(\'Test JavaScript injected.\');" 204 "document.querySelector(\'" + button_class + "\').click();"); 205 // The file selection handler closes the dialog and does not return control 206 // to JavaScript, so do not wait for return values. 207 host->GetMainFrame()->ExecuteJavaScript(script); 208 LOG(INFO) << "Waiting for window close notification."; 209 host_destroyed.Wait(); 210 211 // Dialog no longer believes it is running. 212 ASSERT_FALSE(dialog_->IsRunning(owning_window)); 213 } 214 215 scoped_ptr<MockSelectFileDialogListener> listener_; 216 scoped_refptr<SelectFileDialogExtension> dialog_; 217 218 scoped_ptr<MockSelectFileDialogListener> second_listener_; 219 scoped_refptr<SelectFileDialogExtension> second_dialog_; 220 221 base::ScopedTempDir tmp_dir_; 222 base::FilePath downloads_dir_; 223 }; 224 225 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, CreateAndDestroy) { 226 // Browser window must be up for us to test dialog window parent. 227 gfx::NativeWindow native_window = browser()->window()->GetNativeWindow(); 228 ASSERT_TRUE(native_window != NULL); 229 230 // Before we call SelectFile, dialog is not running/visible. 231 ASSERT_FALSE(dialog_->IsRunning(native_window)); 232 } 233 234 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, DestroyListener) { 235 // Some users of SelectFileDialog destroy their listener before cleaning 236 // up the dialog. Make sure we don't crash. 237 dialog_->ListenerDestroyed(); 238 listener_.reset(); 239 } 240 241 // TODO(jamescook): Add a test for selecting a file for an <input type='file'/> 242 // page element, as that uses different memory management pathways. 243 // crbug.com/98791 244 245 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, 246 SelectFileAndCancel) { 247 AddMountPoint(downloads_dir_); 248 249 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); 250 251 // base::FilePath() for default path. 252 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, 253 base::FilePath(), owning_window, "")); 254 255 // Press cancel button. 256 CloseDialog(DIALOG_BTN_CANCEL, owning_window); 257 258 // Listener should have been informed of the cancellation. 259 ASSERT_FALSE(listener_->file_selected()); 260 ASSERT_TRUE(listener_->canceled()); 261 ASSERT_EQ(this, listener_->params()); 262 } 263 264 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, 265 SelectFileAndOpen) { 266 AddMountPoint(downloads_dir_); 267 268 base::FilePath test_file = 269 downloads_dir_.AppendASCII("file_manager_test.html"); 270 271 // Create an empty file to give us something to select. 272 FILE* fp = base::OpenFile(test_file, "w"); 273 ASSERT_TRUE(fp != NULL); 274 ASSERT_TRUE(base::CloseFile(fp)); 275 276 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); 277 278 // Spawn a dialog to open a file. Provide the path to the file so the dialog 279 // will automatically select it. Ensure that the OK button is enabled by 280 // waiting for chrome.test.sendMessage('selection-change-complete'). 281 // The extension starts a Web Worker to read file metadata, so it may send 282 // 'selection-change-complete' before 'worker-initialized'. This is OK. 283 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, 284 test_file, owning_window, 285 "selection-change-complete")); 286 287 // Click open button. 288 CloseDialog(DIALOG_BTN_OK, owning_window); 289 290 // Listener should have been informed that the file was opened. 291 ASSERT_TRUE(listener_->file_selected()); 292 ASSERT_FALSE(listener_->canceled()); 293 ASSERT_EQ(test_file, listener_->path()); 294 ASSERT_EQ(this, listener_->params()); 295 } 296 297 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, 298 SelectFileAndSave) { 299 AddMountPoint(downloads_dir_); 300 301 base::FilePath test_file = 302 downloads_dir_.AppendASCII("file_manager_test.html"); 303 304 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); 305 306 // Spawn a dialog to save a file, providing a suggested path. 307 // Ensure "Save" button is enabled by waiting for notification from 308 // chrome.test.sendMessage(). 309 // The extension starts a Web Worker to read file metadata, so it may send 310 // 'directory-change-complete' before 'worker-initialized'. This is OK. 311 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE, 312 test_file, owning_window, 313 "directory-change-complete")); 314 315 // Click save button. 316 CloseDialog(DIALOG_BTN_OK, owning_window); 317 318 // Listener should have been informed that the file was selected. 319 ASSERT_TRUE(listener_->file_selected()); 320 ASSERT_FALSE(listener_->canceled()); 321 ASSERT_EQ(test_file, listener_->path()); 322 ASSERT_EQ(this, listener_->params()); 323 } 324 325 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, 326 OpenSingletonTabAndCancel) { 327 AddMountPoint(downloads_dir_); 328 329 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); 330 331 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, 332 base::FilePath(), owning_window, "")); 333 334 // Open a singleton tab in background. 335 chrome::NavigateParams p(browser(), GURL("http://www.google.com"), 336 content::PAGE_TRANSITION_LINK); 337 p.window_action = chrome::NavigateParams::SHOW_WINDOW; 338 p.disposition = SINGLETON_TAB; 339 chrome::Navigate(&p); 340 341 // Press cancel button. 342 CloseDialog(DIALOG_BTN_CANCEL, owning_window); 343 344 // Listener should have been informed of the cancellation. 345 ASSERT_FALSE(listener_->file_selected()); 346 ASSERT_TRUE(listener_->canceled()); 347 ASSERT_EQ(this, listener_->params()); 348 } 349 350 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, 351 OpenTwoDialogs) { 352 AddMountPoint(downloads_dir_); 353 354 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow(); 355 356 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE, 357 base::FilePath(), owning_window, "")); 358 359 TryOpeningSecondDialog(owning_window); 360 361 // Second dialog should not be running. 362 ASSERT_FALSE(second_dialog_->IsRunning(owning_window)); 363 364 // Click cancel button. 365 CloseDialog(DIALOG_BTN_CANCEL, owning_window); 366 367 // Listener should have been informed of the cancellation. 368 ASSERT_FALSE(listener_->file_selected()); 369 ASSERT_TRUE(listener_->canceled()); 370 ASSERT_EQ(this, listener_->params()); 371 } 372