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/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"
34 using content::BrowserContext
;
36 // Mock listener used by test below.
37 class MockSelectFileDialogListener
: public ui::SelectFileDialog::Listener
{
39 MockSelectFileDialogListener()
40 : file_selected_(false),
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_
; }
50 // ui::SelectFileDialog::Listener implementation.
51 virtual void FileSelected(const base::FilePath
& path
,
53 void* params
) OVERRIDE
{
54 file_selected_
= true;
58 virtual void FileSelectedWithExtraInfo(
59 const ui::SelectedFileInfo
& selected_file_info
,
61 void* params
) OVERRIDE
{
62 FileSelected(selected_file_info
.local_path
, index
, params
);
64 virtual void MultiFilesSelected(
65 const std::vector
<base::FilePath
>& files
, void* params
) OVERRIDE
{}
66 virtual void FileSelectionCanceled(void* params
) OVERRIDE
{
77 DISALLOW_COPY_AND_ASSIGN(MockSelectFileDialogListener
);
80 class SelectFileDialogExtensionBrowserTest
: public ExtensionBrowserTest
{
82 enum DialogButtonType
{
87 virtual void SetUp() OVERRIDE
{
88 extensions::ComponentLoader::EnableBackgroundExtensionsForTesting();
90 // Create the dialog wrapper object, but don't show it yet.
91 listener_
.reset(new MockSelectFileDialogListener());
92 dialog_
= new SelectFileDialogExtension(listener_
.get(), NULL
);
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_
);
102 // Must run after our setup because it actually runs the test.
103 ExtensionBrowserTest::SetUp();
106 virtual void TearDown() OVERRIDE
{
107 ExtensionBrowserTest::TearDown();
109 // Delete the dialog first, as it holds a pointer to the listener.
113 second_dialog_
= NULL
;
114 second_listener_
.reset();
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_
);
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
);
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 */);
144 scoped_ptr
<ExtensionTestMessageListener
> additional_listener
;
145 if (!additional_message
.empty()) {
146 additional_listener
.reset(
147 new ExtensionTestMessageListener(additional_message
, false));
150 dialog_
->SelectFile(dialog_type
,
151 base::string16() /* title */,
153 NULL
/* file_types */,
154 0 /* file_type_index */,
155 FILE_PATH_LITERAL("") /* default_extension */,
159 LOG(INFO
) << "Waiting for JavaScript ready message.";
160 ASSERT_TRUE(init_listener
.WaitUntilSatisfied());
162 if (additional_listener
.get()) {
163 LOG(INFO
) << "Waiting for JavaScript " << additional_message
165 ASSERT_TRUE(additional_listener
->WaitUntilSatisfied());
168 // Dialog should be running now.
169 ASSERT_TRUE(dialog_
->IsRunning(owning_window
));
171 ASSERT_NO_FATAL_FAILURE(CheckJavascriptErrors());
174 void TryOpeningSecondDialog(const gfx::NativeWindow
& owning_window
) {
175 second_listener_
.reset(new MockSelectFileDialogListener());
176 second_dialog_
= new SelectFileDialogExtension(second_listener_
.get(),
179 // At the moment we don't really care about dialog type, but we have to put
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 */,
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();
211 // Dialog no longer believes it is running.
212 ASSERT_FALSE(dialog_
->IsRunning(owning_window
));
215 scoped_ptr
<MockSelectFileDialogListener
> listener_
;
216 scoped_refptr
<SelectFileDialogExtension
> dialog_
;
218 scoped_ptr
<MockSelectFileDialogListener
> second_listener_
;
219 scoped_refptr
<SelectFileDialogExtension
> second_dialog_
;
221 base::ScopedTempDir tmp_dir_
;
222 base::FilePath downloads_dir_
;
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
);
230 // Before we call SelectFile, dialog is not running/visible.
231 ASSERT_FALSE(dialog_
->IsRunning(native_window
));
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();
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.
245 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
246 SelectFileAndCancel
) {
247 AddMountPoint(downloads_dir_
);
249 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
251 // base::FilePath() for default path.
252 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE
,
253 base::FilePath(), owning_window
, ""));
255 // Press cancel button.
256 CloseDialog(DIALOG_BTN_CANCEL
, owning_window
);
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());
264 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
266 AddMountPoint(downloads_dir_
);
268 base::FilePath test_file
=
269 downloads_dir_
.AppendASCII("file_manager_test.html");
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
));
276 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
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"));
287 // Click open button.
288 CloseDialog(DIALOG_BTN_OK
, owning_window
);
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());
297 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
299 AddMountPoint(downloads_dir_
);
301 base::FilePath test_file
=
302 downloads_dir_
.AppendASCII("file_manager_test.html");
304 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
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"));
315 // Click save button.
316 CloseDialog(DIALOG_BTN_OK
, owning_window
);
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());
325 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
326 OpenSingletonTabAndCancel
) {
327 AddMountPoint(downloads_dir_
);
329 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
331 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE
,
332 base::FilePath(), owning_window
, ""));
334 // Open a singleton tab in background.
335 chrome::NavigateParams
p(browser(), GURL("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
);
341 // Press cancel button.
342 CloseDialog(DIALOG_BTN_CANCEL
, owning_window
);
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());
350 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
352 AddMountPoint(downloads_dir_
);
354 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
356 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE
,
357 base::FilePath(), owning_window
, ""));
359 TryOpeningSecondDialog(owning_window
);
361 // Second dialog should not be running.
362 ASSERT_FALSE(second_dialog_
->IsRunning(owning_window
));
364 // Click cancel button.
365 CloseDialog(DIALOG_BTN_CANCEL
, owning_window
);
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());