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/files/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/profiles/profile.h"
20 #include "chrome/browser/ui/browser.h"
21 #include "chrome/browser/ui/browser_navigator.h"
22 #include "chrome/browser/ui/browser_window.h"
23 #include "chrome/common/chrome_paths.h"
24 #include "chrome/common/pref_names.h"
25 #include "content/public/browser/render_frame_host.h"
26 #include "content/public/browser/render_view_host.h"
27 #include "content/public/test/test_utils.h"
28 #include "extensions/test/extension_test_message_listener.h"
29 #include "ui/shell_dialogs/select_file_dialog.h"
30 #include "ui/shell_dialogs/selected_file_info.h"
32 using content::BrowserContext
;
34 // Mock listener used by test below.
35 class MockSelectFileDialogListener
: public ui::SelectFileDialog::Listener
{
37 MockSelectFileDialogListener()
38 : file_selected_(false),
43 bool file_selected() const { return file_selected_
; }
44 bool canceled() const { return canceled_
; }
45 base::FilePath
path() const { return path_
; }
46 void* params() const { return params_
; }
48 // ui::SelectFileDialog::Listener implementation.
49 void FileSelected(const base::FilePath
& path
,
51 void* params
) override
{
52 file_selected_
= true;
57 void FileSelectedWithExtraInfo(const ui::SelectedFileInfo
& selected_file_info
,
59 void* params
) override
{
60 FileSelected(selected_file_info
.local_path
, index
, params
);
62 void MultiFilesSelected(const std::vector
<base::FilePath
>& files
,
63 void* params
) override
{
66 void FileSelectionCanceled(void* params
) override
{
72 void WaitForCalled() {
73 message_loop_runner_
= new content::MessageLoopRunner();
74 message_loop_runner_
->Run();
78 void QuitMessageLoop() {
79 if (message_loop_runner_
.get())
80 message_loop_runner_
->Quit();
87 scoped_refptr
<content::MessageLoopRunner
> message_loop_runner_
;
89 DISALLOW_COPY_AND_ASSIGN(MockSelectFileDialogListener
);
92 class SelectFileDialogExtensionBrowserTest
: public ExtensionBrowserTest
{
94 enum DialogButtonType
{
99 void SetUp() override
{
100 extensions::ComponentLoader::EnableBackgroundExtensionsForTesting();
102 // Create the dialog wrapper object, but don't show it yet.
103 listener_
.reset(new MockSelectFileDialogListener());
104 dialog_
= new SelectFileDialogExtension(listener_
.get(), NULL
);
106 // We have to provide at least one mount point.
107 // File manager looks for "Downloads" mount point, so use this name.
108 base::FilePath tmp_path
;
109 PathService::Get(base::DIR_TEMP
, &tmp_path
);
110 ASSERT_TRUE(tmp_dir_
.CreateUniqueTempDirUnderPath(tmp_path
));
111 downloads_dir_
= tmp_dir_
.path().Append("Downloads");
112 base::CreateDirectory(downloads_dir_
);
114 // Must run after our setup because it actually runs the test.
115 ExtensionBrowserTest::SetUp();
118 void TearDown() override
{
119 ExtensionBrowserTest::TearDown();
121 // Delete the dialog first, as it holds a pointer to the listener.
125 second_dialog_
= NULL
;
126 second_listener_
.reset();
129 // Creates a file system mount point for a directory.
130 void AddMountPoint(const base::FilePath
& path
) {
131 EXPECT_TRUE(file_manager::VolumeManager::Get(
132 browser()->profile())->RegisterDownloadsDirectoryForTesting(path
));
133 browser()->profile()->GetPrefs()->SetFilePath(
134 prefs::kDownloadDefaultDirectory
, downloads_dir_
);
137 void CheckJavascriptErrors() {
138 content::RenderFrameHost
* host
=
139 dialog_
->GetRenderViewHost()->GetMainFrame();
140 scoped_ptr
<base::Value
> value
=
141 content::ExecuteScriptAndGetValue(host
, "window.JSErrorCount");
142 int js_error_count
= 0;
143 ASSERT_TRUE(value
->GetAsInteger(&js_error_count
));
144 ASSERT_EQ(0, js_error_count
);
147 void OpenDialog(ui::SelectFileDialog::Type dialog_type
,
148 const base::FilePath
& file_path
,
149 const gfx::NativeWindow
& owning_window
,
150 const std::string
& additional_message
) {
151 // Spawn a dialog to open a file. The dialog will signal that it is ready
152 // via chrome.test.sendMessage() in the extension JavaScript.
153 ExtensionTestMessageListener
init_listener("ready", false /* will_reply */);
155 scoped_ptr
<ExtensionTestMessageListener
> additional_listener
;
156 if (!additional_message
.empty()) {
157 additional_listener
.reset(
158 new ExtensionTestMessageListener(additional_message
, false));
161 dialog_
->SelectFile(dialog_type
,
162 base::string16() /* title */,
164 NULL
/* file_types */,
165 0 /* file_type_index */,
166 FILE_PATH_LITERAL("") /* default_extension */,
170 LOG(INFO
) << "Waiting for JavaScript ready message.";
171 ASSERT_TRUE(init_listener
.WaitUntilSatisfied());
173 if (additional_listener
.get()) {
174 LOG(INFO
) << "Waiting for JavaScript " << additional_message
176 ASSERT_TRUE(additional_listener
->WaitUntilSatisfied());
179 // Dialog should be running now.
180 ASSERT_TRUE(dialog_
->IsRunning(owning_window
));
182 ASSERT_NO_FATAL_FAILURE(CheckJavascriptErrors());
185 void TryOpeningSecondDialog(const gfx::NativeWindow
& owning_window
) {
186 second_listener_
.reset(new MockSelectFileDialogListener());
187 second_dialog_
= new SelectFileDialogExtension(second_listener_
.get(),
190 // At the moment we don't really care about dialog type, but we have to put
192 second_dialog_
->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE
,
193 base::string16() /* title */,
194 base::FilePath() /* default_path */,
195 NULL
/* file_types */,
196 0 /* file_type_index */,
197 FILE_PATH_LITERAL("") /* default_extension */,
202 void CloseDialog(DialogButtonType button_type
,
203 const gfx::NativeWindow
& owning_window
) {
204 // Inject JavaScript to click the cancel button and wait for notification
205 // that the window has closed.
206 content::RenderViewHost
* host
= dialog_
->GetRenderViewHost();
207 std::string button_class
=
208 (button_type
== DIALOG_BTN_OK
) ? ".button-panel .ok" :
209 ".button-panel .cancel";
210 base::string16 script
= base::ASCIIToUTF16(
211 "console.log(\'Test JavaScript injected.\');"
212 "document.querySelector(\'" + button_class
+ "\').click();");
213 // The file selection handler closes the dialog and does not return control
214 // to JavaScript, so do not wait for return values.
215 host
->GetMainFrame()->ExecuteJavaScript(script
);
216 LOG(INFO
) << "Waiting for window close notification.";
217 listener_
->WaitForCalled();
219 // Dialog no longer believes it is running.
220 ASSERT_FALSE(dialog_
->IsRunning(owning_window
));
223 scoped_ptr
<MockSelectFileDialogListener
> listener_
;
224 scoped_refptr
<SelectFileDialogExtension
> dialog_
;
226 scoped_ptr
<MockSelectFileDialogListener
> second_listener_
;
227 scoped_refptr
<SelectFileDialogExtension
> second_dialog_
;
229 base::ScopedTempDir tmp_dir_
;
230 base::FilePath downloads_dir_
;
233 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
, CreateAndDestroy
) {
234 // Browser window must be up for us to test dialog window parent.
235 gfx::NativeWindow native_window
= browser()->window()->GetNativeWindow();
236 ASSERT_TRUE(native_window
!= NULL
);
238 // Before we call SelectFile, dialog is not running/visible.
239 ASSERT_FALSE(dialog_
->IsRunning(native_window
));
242 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
, DestroyListener
) {
243 // Some users of SelectFileDialog destroy their listener before cleaning
244 // up the dialog. Make sure we don't crash.
245 dialog_
->ListenerDestroyed();
249 // TODO(jamescook): Add a test for selecting a file for an <input type='file'/>
250 // page element, as that uses different memory management pathways.
253 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
254 SelectFileAndCancel
) {
255 AddMountPoint(downloads_dir_
);
257 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
259 // base::FilePath() for default path.
260 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE
,
261 base::FilePath(), owning_window
, ""));
263 // Press cancel button.
264 CloseDialog(DIALOG_BTN_CANCEL
, owning_window
);
266 // Listener should have been informed of the cancellation.
267 ASSERT_FALSE(listener_
->file_selected());
268 ASSERT_TRUE(listener_
->canceled());
269 ASSERT_EQ(this, listener_
->params());
272 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
274 AddMountPoint(downloads_dir_
);
276 base::FilePath test_file
=
277 downloads_dir_
.AppendASCII("file_manager_test.html");
279 // Create an empty file to give us something to select.
280 FILE* fp
= base::OpenFile(test_file
, "w");
281 ASSERT_TRUE(fp
!= NULL
);
282 ASSERT_TRUE(base::CloseFile(fp
));
284 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
286 // Spawn a dialog to open a file. Provide the path to the file so the dialog
287 // will automatically select it. Ensure that the OK button is enabled by
288 // waiting for chrome.test.sendMessage('selection-change-complete').
289 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE
,
290 test_file
, owning_window
,
293 // Click open button.
294 CloseDialog(DIALOG_BTN_OK
, owning_window
);
296 // Listener should have been informed that the file was opened.
297 ASSERT_TRUE(listener_
->file_selected());
298 ASSERT_FALSE(listener_
->canceled());
299 ASSERT_EQ(test_file
, listener_
->path());
300 ASSERT_EQ(this, listener_
->params());
303 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
305 AddMountPoint(downloads_dir_
);
307 base::FilePath test_file
=
308 downloads_dir_
.AppendASCII("file_manager_test.html");
310 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
312 // Spawn a dialog to save a file, providing a suggested path.
313 // Ensure "Save" button is enabled by waiting for notification from
314 // chrome.test.sendMessage().
315 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE
,
316 test_file
, owning_window
,
319 // Click save button.
320 CloseDialog(DIALOG_BTN_OK
, owning_window
);
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());
329 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
330 OpenSingletonTabAndCancel
) {
331 AddMountPoint(downloads_dir_
);
333 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
335 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE
,
336 base::FilePath(), owning_window
, ""));
338 // Open a singleton tab in background.
339 chrome::NavigateParams
p(browser(), GURL("http://www.google.com"),
340 ui::PAGE_TRANSITION_LINK
);
341 p
.window_action
= chrome::NavigateParams::SHOW_WINDOW
;
342 p
.disposition
= SINGLETON_TAB
;
343 chrome::Navigate(&p
);
345 // Press cancel button.
346 CloseDialog(DIALOG_BTN_CANCEL
, owning_window
);
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());
354 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest
,
356 AddMountPoint(downloads_dir_
);
358 gfx::NativeWindow owning_window
= browser()->window()->GetNativeWindow();
360 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE
,
361 base::FilePath(), owning_window
, ""));
363 TryOpeningSecondDialog(owning_window
);
365 // Second dialog should not be running.
366 ASSERT_FALSE(second_dialog_
->IsRunning(owning_window
));
368 // Click cancel button.
369 CloseDialog(DIALOG_BTN_CANCEL
, owning_window
);
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());