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.
5 #include "chrome/browser/ui/views/select_file_dialog_extension.h"
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"
33 using content::BrowserContext;
35 // Mock listener used by test below.
36 class MockSelectFileDialogListener : public ui::SelectFileDialog::Listener {
38 MockSelectFileDialogListener()
39 : file_selected_(false),
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_; }
49 // ui::SelectFileDialog::Listener implementation.
50 virtual void FileSelected(const base::FilePath& path,
52 void* params) OVERRIDE {
53 file_selected_ = true;
57 virtual void FileSelectedWithExtraInfo(
58 const ui::SelectedFileInfo& selected_file_info,
60 void* params) OVERRIDE {
61 FileSelected(selected_file_info.local_path, index, params);
63 virtual void MultiFilesSelected(
64 const std::vector<base::FilePath>& files, void* params) OVERRIDE {}
65 virtual void FileSelectionCanceled(void* params) OVERRIDE {
76 DISALLOW_COPY_AND_ASSIGN(MockSelectFileDialogListener);
79 class SelectFileDialogExtensionBrowserTest : public ExtensionBrowserTest {
81 enum DialogButtonType {
86 virtual void SetUp() OVERRIDE {
87 extensions::ComponentLoader::EnableBackgroundExtensionsForTesting();
89 // Create the dialog wrapper object, but don't show it yet.
90 listener_.reset(new MockSelectFileDialogListener());
91 dialog_ = new SelectFileDialogExtension(listener_.get(), NULL);
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 file_util::CreateDirectory(downloads_dir_);
101 // Must run after our setup because it actually runs the test.
102 ExtensionBrowserTest::SetUp();
105 virtual void TearDown() OVERRIDE {
106 ExtensionBrowserTest::TearDown();
108 // Delete the dialog first, as it holds a pointer to the listener.
112 second_dialog_ = NULL;
113 second_listener_.reset();
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, path));
128 void CheckJavascriptErrors() {
129 content::RenderViewHost* host = dialog_->GetRenderViewHost();
130 scoped_ptr<base::Value> value =
131 content::ExecuteScriptAndGetValue(host, "window.JSErrorCount");
132 int js_error_count = 0;
133 ASSERT_TRUE(value->GetAsInteger(&js_error_count));
134 ASSERT_EQ(0, js_error_count);
137 void OpenDialog(ui::SelectFileDialog::Type dialog_type,
138 const base::FilePath& file_path,
139 const gfx::NativeWindow& owning_window,
140 const std::string& additional_message) {
141 // Spawn a dialog to open a file. The dialog will signal that it is ready
142 // via chrome.test.sendMessage() in the extension JavaScript.
143 ExtensionTestMessageListener init_listener("worker-initialized",
144 false /* will_reply */);
146 scoped_ptr<ExtensionTestMessageListener> additional_listener;
147 if (!additional_message.empty()) {
148 additional_listener.reset(
149 new ExtensionTestMessageListener(additional_message, false));
152 dialog_->SelectFile(dialog_type,
153 string16() /* title */,
155 NULL /* file_types */,
156 0 /* file_type_index */,
157 FILE_PATH_LITERAL("") /* default_extension */,
161 LOG(INFO) << "Waiting for JavaScript ready message.";
162 ASSERT_TRUE(init_listener.WaitUntilSatisfied());
164 if (additional_listener.get()) {
165 LOG(INFO) << "Waiting for JavaScript " << additional_message
167 ASSERT_TRUE(additional_listener->WaitUntilSatisfied());
170 // Dialog should be running now.
171 ASSERT_TRUE(dialog_->IsRunning(owning_window));
173 ASSERT_NO_FATAL_FAILURE(CheckJavascriptErrors());
176 void TryOpeningSecondDialog(const gfx::NativeWindow& owning_window) {
177 second_listener_.reset(new MockSelectFileDialogListener());
178 second_dialog_ = new SelectFileDialogExtension(second_listener_.get(),
181 // At the moment we don't really care about dialog type, but we have to put
183 second_dialog_->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE,
184 string16() /* title */,
185 base::FilePath() /* default_path */,
186 NULL /* file_types */,
187 0 /* file_type_index */,
188 FILE_PATH_LITERAL("") /* default_extension */,
193 void CloseDialog(DialogButtonType button_type,
194 const gfx::NativeWindow& owning_window) {
195 // Inject JavaScript to click the cancel button and wait for notification
196 // that the window has closed.
197 content::WindowedNotificationObserver host_destroyed(
198 content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED,
199 content::NotificationService::AllSources());
200 content::RenderViewHost* host = dialog_->GetRenderViewHost();
202 std::string button_class =
203 (button_type == DIALOG_BTN_OK) ? ".button-panel .ok" :
204 ".button-panel .cancel";
205 string16 script = ASCIIToUTF16(
206 "console.log(\'Test JavaScript injected.\');"
207 "document.querySelector(\'" + button_class + "\').click();");
208 // The file selection handler closes the dialog and does not return control
209 // to JavaScript, so do not wait for return values.
210 host->ExecuteJavascriptInWebFrame(main_frame, script);
211 LOG(INFO) << "Waiting for window close notification.";
212 host_destroyed.Wait();
214 // Dialog no longer believes it is running.
215 ASSERT_FALSE(dialog_->IsRunning(owning_window));
218 scoped_ptr<MockSelectFileDialogListener> listener_;
219 scoped_refptr<SelectFileDialogExtension> dialog_;
221 scoped_ptr<MockSelectFileDialogListener> second_listener_;
222 scoped_refptr<SelectFileDialogExtension> second_dialog_;
224 base::ScopedTempDir tmp_dir_;
225 base::FilePath downloads_dir_;
228 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, CreateAndDestroy) {
229 // Browser window must be up for us to test dialog window parent.
230 gfx::NativeWindow native_window = browser()->window()->GetNativeWindow();
231 ASSERT_TRUE(native_window != NULL);
233 // Before we call SelectFile, dialog is not running/visible.
234 ASSERT_FALSE(dialog_->IsRunning(native_window));
237 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, DestroyListener) {
238 // Some users of SelectFileDialog destroy their listener before cleaning
239 // up the dialog. Make sure we don't crash.
240 dialog_->ListenerDestroyed();
244 // TODO(jamescook): Add a test for selecting a file for an <input type='file'/>
245 // page element, as that uses different memory management pathways.
248 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
249 SelectFileAndCancel) {
250 AddMountPoint(downloads_dir_);
252 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
254 // base::FilePath() for default path.
255 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
256 base::FilePath(), owning_window, ""));
258 // Press cancel button.
259 CloseDialog(DIALOG_BTN_CANCEL, owning_window);
261 // Listener should have been informed of the cancellation.
262 ASSERT_FALSE(listener_->file_selected());
263 ASSERT_TRUE(listener_->canceled());
264 ASSERT_EQ(this, listener_->params());
267 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
269 AddMountPoint(downloads_dir_);
271 base::FilePath test_file =
272 downloads_dir_.AppendASCII("file_manager_test.html");
274 // Create an empty file to give us something to select.
275 FILE* fp = file_util::OpenFile(test_file, "w");
276 ASSERT_TRUE(fp != NULL);
277 ASSERT_TRUE(file_util::CloseFile(fp));
279 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
281 // Spawn a dialog to open a file. Provide the path to the file so the dialog
282 // will automatically select it. Ensure that the OK button is enabled by
283 // waiting for chrome.test.sendMessage('selection-change-complete').
284 // The extension starts a Web Worker to read file metadata, so it may send
285 // 'selection-change-complete' before 'worker-initialized'. This is OK.
286 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
287 test_file, owning_window,
288 "selection-change-complete"));
290 // Click open button.
291 CloseDialog(DIALOG_BTN_OK, owning_window);
293 // Listener should have been informed that the file was opened.
294 ASSERT_TRUE(listener_->file_selected());
295 ASSERT_FALSE(listener_->canceled());
296 ASSERT_EQ(test_file, listener_->path());
297 ASSERT_EQ(this, listener_->params());
300 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
302 AddMountPoint(downloads_dir_);
304 base::FilePath test_file =
305 downloads_dir_.AppendASCII("file_manager_test.html");
307 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
309 // Spawn a dialog to save a file, providing a suggested path.
310 // Ensure "Save" button is enabled by waiting for notification from
311 // chrome.test.sendMessage().
312 // The extension starts a Web Worker to read file metadata, so it may send
313 // 'directory-change-complete' before 'worker-initialized'. This is OK.
314 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE,
315 test_file, owning_window,
316 "directory-change-complete"));
318 // Click save button.
319 CloseDialog(DIALOG_BTN_OK, owning_window);
321 // Listener should have been informed that the file was selected.
322 ASSERT_TRUE(listener_->file_selected());
323 ASSERT_FALSE(listener_->canceled());
324 ASSERT_EQ(test_file, listener_->path());
325 ASSERT_EQ(this, listener_->params());
328 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
329 OpenSingletonTabAndCancel) {
330 AddMountPoint(downloads_dir_);
332 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
334 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
335 base::FilePath(), owning_window, ""));
337 // Open a singleton tab in background.
338 chrome::NavigateParams p(browser(), GURL("www.google.com"),
339 content::PAGE_TRANSITION_LINK);
340 p.window_action = chrome::NavigateParams::SHOW_WINDOW;
341 p.disposition = SINGLETON_TAB;
342 chrome::Navigate(&p);
344 // Press cancel button.
345 CloseDialog(DIALOG_BTN_CANCEL, owning_window);
347 // Listener should have been informed of the cancellation.
348 ASSERT_FALSE(listener_->file_selected());
349 ASSERT_TRUE(listener_->canceled());
350 ASSERT_EQ(this, listener_->params());
353 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
355 AddMountPoint(downloads_dir_);
357 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
359 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
360 base::FilePath(), owning_window, ""));
362 TryOpeningSecondDialog(owning_window);
364 // Second dialog should not be running.
365 ASSERT_FALSE(second_dialog_->IsRunning(owning_window));
367 // Click cancel button.
368 CloseDialog(DIALOG_BTN_CANCEL, owning_window);
370 // Listener should have been informed of the cancellation.
371 ASSERT_FALSE(listener_->file_selected());
372 ASSERT_TRUE(listener_->canceled());
373 ASSERT_EQ(this, listener_->params());