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