select_file_dialog_extension_browsertest.cc revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
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
33using content::BrowserContext;
34
35// Mock listener used by test below.
36class 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
79class 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
229IN_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
238IN_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
249IN_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
268IN_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
301IN_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
329IN_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
354IN_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