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/notification_service.h"
26 #include "content/public/browser/notification_types.h"
27 #include "content/public/browser/render_frame_host.h"
28 #include "content/public/browser/render_view_host.h"
29 #include "content/public/test/test_utils.h"
30 #include "extensions/test/extension_test_message_listener.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("ready", false /* will_reply */);
143 scoped_ptr<ExtensionTestMessageListener> additional_listener;
144 if (!additional_message.empty()) {
145 additional_listener.reset(
146 new ExtensionTestMessageListener(additional_message, false));
149 dialog_->SelectFile(dialog_type,
150 base::string16() /* title */,
152 NULL /* file_types */,
153 0 /* file_type_index */,
154 FILE_PATH_LITERAL("") /* default_extension */,
158 LOG(INFO) << "Waiting for JavaScript ready message.";
159 ASSERT_TRUE(init_listener.WaitUntilSatisfied());
161 if (additional_listener.get()) {
162 LOG(INFO) << "Waiting for JavaScript " << additional_message
164 ASSERT_TRUE(additional_listener->WaitUntilSatisfied());
167 // Dialog should be running now.
168 ASSERT_TRUE(dialog_->IsRunning(owning_window));
170 ASSERT_NO_FATAL_FAILURE(CheckJavascriptErrors());
173 void TryOpeningSecondDialog(const gfx::NativeWindow& owning_window) {
174 second_listener_.reset(new MockSelectFileDialogListener());
175 second_dialog_ = new SelectFileDialogExtension(second_listener_.get(),
178 // At the moment we don't really care about dialog type, but we have to put
180 second_dialog_->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE,
181 base::string16() /* title */,
182 base::FilePath() /* default_path */,
183 NULL /* file_types */,
184 0 /* file_type_index */,
185 FILE_PATH_LITERAL("") /* default_extension */,
190 void CloseDialog(DialogButtonType button_type,
191 const gfx::NativeWindow& owning_window) {
192 // Inject JavaScript to click the cancel button and wait for notification
193 // that the window has closed.
194 content::WindowedNotificationObserver host_destroyed(
195 content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED,
196 content::NotificationService::AllSources());
197 content::RenderViewHost* host = dialog_->GetRenderViewHost();
198 std::string button_class =
199 (button_type == DIALOG_BTN_OK) ? ".button-panel .ok" :
200 ".button-panel .cancel";
201 base::string16 script = base::ASCIIToUTF16(
202 "console.log(\'Test JavaScript injected.\');"
203 "document.querySelector(\'" + button_class + "\').click();");
204 // The file selection handler closes the dialog and does not return control
205 // to JavaScript, so do not wait for return values.
206 host->GetMainFrame()->ExecuteJavaScript(script);
207 LOG(INFO) << "Waiting for window close notification.";
208 host_destroyed.Wait();
210 // Dialog no longer believes it is running.
211 ASSERT_FALSE(dialog_->IsRunning(owning_window));
214 scoped_ptr<MockSelectFileDialogListener> listener_;
215 scoped_refptr<SelectFileDialogExtension> dialog_;
217 scoped_ptr<MockSelectFileDialogListener> second_listener_;
218 scoped_refptr<SelectFileDialogExtension> second_dialog_;
220 base::ScopedTempDir tmp_dir_;
221 base::FilePath downloads_dir_;
224 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, CreateAndDestroy) {
225 // Browser window must be up for us to test dialog window parent.
226 gfx::NativeWindow native_window = browser()->window()->GetNativeWindow();
227 ASSERT_TRUE(native_window != NULL);
229 // Before we call SelectFile, dialog is not running/visible.
230 ASSERT_FALSE(dialog_->IsRunning(native_window));
233 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest, DestroyListener) {
234 // Some users of SelectFileDialog destroy their listener before cleaning
235 // up the dialog. Make sure we don't crash.
236 dialog_->ListenerDestroyed();
240 // TODO(jamescook): Add a test for selecting a file for an <input type='file'/>
241 // page element, as that uses different memory management pathways.
244 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
245 SelectFileAndCancel) {
246 AddMountPoint(downloads_dir_);
248 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
250 // base::FilePath() for default path.
251 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
252 base::FilePath(), owning_window, ""));
254 // Press cancel button.
255 CloseDialog(DIALOG_BTN_CANCEL, owning_window);
257 // Listener should have been informed of the cancellation.
258 ASSERT_FALSE(listener_->file_selected());
259 ASSERT_TRUE(listener_->canceled());
260 ASSERT_EQ(this, listener_->params());
263 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
265 AddMountPoint(downloads_dir_);
267 base::FilePath test_file =
268 downloads_dir_.AppendASCII("file_manager_test.html");
270 // Create an empty file to give us something to select.
271 FILE* fp = base::OpenFile(test_file, "w");
272 ASSERT_TRUE(fp != NULL);
273 ASSERT_TRUE(base::CloseFile(fp));
275 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
277 // Spawn a dialog to open a file. Provide the path to the file so the dialog
278 // will automatically select it. Ensure that the OK button is enabled by
279 // waiting for chrome.test.sendMessage('selection-change-complete').
280 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
281 test_file, owning_window,
282 "selection-change-complete"));
284 // Click open button.
285 CloseDialog(DIALOG_BTN_OK, owning_window);
287 // Listener should have been informed that the file was opened.
288 ASSERT_TRUE(listener_->file_selected());
289 ASSERT_FALSE(listener_->canceled());
290 ASSERT_EQ(test_file, listener_->path());
291 ASSERT_EQ(this, listener_->params());
294 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
296 AddMountPoint(downloads_dir_);
298 base::FilePath test_file =
299 downloads_dir_.AppendASCII("file_manager_test.html");
301 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
303 // Spawn a dialog to save a file, providing a suggested path.
304 // Ensure "Save" button is enabled by waiting for notification from
305 // chrome.test.sendMessage().
306 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_SAVEAS_FILE,
307 test_file, owning_window,
308 "directory-change-complete"));
310 // Click save button.
311 CloseDialog(DIALOG_BTN_OK, owning_window);
313 // Listener should have been informed that the file was selected.
314 ASSERT_TRUE(listener_->file_selected());
315 ASSERT_FALSE(listener_->canceled());
316 ASSERT_EQ(test_file, listener_->path());
317 ASSERT_EQ(this, listener_->params());
320 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
321 OpenSingletonTabAndCancel) {
322 AddMountPoint(downloads_dir_);
324 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
326 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
327 base::FilePath(), owning_window, ""));
329 // Open a singleton tab in background.
330 chrome::NavigateParams p(browser(), GURL("http://www.google.com"),
331 ui::PAGE_TRANSITION_LINK);
332 p.window_action = chrome::NavigateParams::SHOW_WINDOW;
333 p.disposition = SINGLETON_TAB;
334 chrome::Navigate(&p);
336 // Press cancel button.
337 CloseDialog(DIALOG_BTN_CANCEL, owning_window);
339 // Listener should have been informed of the cancellation.
340 ASSERT_FALSE(listener_->file_selected());
341 ASSERT_TRUE(listener_->canceled());
342 ASSERT_EQ(this, listener_->params());
345 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
347 AddMountPoint(downloads_dir_);
349 gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
351 ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
352 base::FilePath(), owning_window, ""));
354 TryOpeningSecondDialog(owning_window);
356 // Second dialog should not be running.
357 ASSERT_FALSE(second_dialog_->IsRunning(owning_window));
359 // Click cancel button.
360 CloseDialog(DIALOG_BTN_CANCEL, owning_window);
362 // Listener should have been informed of the cancellation.
363 ASSERT_FALSE(listener_->file_selected());
364 ASSERT_TRUE(listener_->canceled());
365 ASSERT_EQ(this, listener_->params());