Home | History | Annotate | Download | only in views
      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 
     33 using content::BrowserContext;
     34 
     35 // Mock listener used by test below.
     36 class 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 
     79 class 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 
    229 IN_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 
    238 IN_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 
    249 IN_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 
    268 IN_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 
    301 IN_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 
    329 IN_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 
    354 IN_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