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/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"
     33 
     34 using content::BrowserContext;
     35 
     36 // Mock listener used by test below.
     37 class MockSelectFileDialogListener : public ui::SelectFileDialog::Listener {
     38  public:
     39   MockSelectFileDialogListener()
     40     : file_selected_(false),
     41       canceled_(false),
     42       params_(NULL) {
     43   }
     44 
     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_; }
     49 
     50   // ui::SelectFileDialog::Listener implementation.
     51   virtual void FileSelected(const base::FilePath& path,
     52                             int index,
     53                             void* params) OVERRIDE {
     54     file_selected_ = true;
     55     path_ = path;
     56     params_ = params;
     57   }
     58   virtual void FileSelectedWithExtraInfo(
     59       const ui::SelectedFileInfo& selected_file_info,
     60       int index,
     61       void* params) OVERRIDE {
     62     FileSelected(selected_file_info.local_path, index, params);
     63   }
     64   virtual void MultiFilesSelected(
     65       const std::vector<base::FilePath>& files, void* params) OVERRIDE {}
     66   virtual void FileSelectionCanceled(void* params) OVERRIDE {
     67     canceled_ = true;
     68     params_ = params;
     69   }
     70 
     71  private:
     72   bool file_selected_;
     73   bool canceled_;
     74   base::FilePath path_;
     75   void* params_;
     76 
     77   DISALLOW_COPY_AND_ASSIGN(MockSelectFileDialogListener);
     78 };
     79 
     80 class SelectFileDialogExtensionBrowserTest : public ExtensionBrowserTest {
     81  public:
     82   enum DialogButtonType {
     83     DIALOG_BTN_OK,
     84     DIALOG_BTN_CANCEL
     85   };
     86 
     87   virtual void SetUp() OVERRIDE {
     88     extensions::ComponentLoader::EnableBackgroundExtensionsForTesting();
     89 
     90     // Create the dialog wrapper object, but don't show it yet.
     91     listener_.reset(new MockSelectFileDialogListener());
     92     dialog_ = new SelectFileDialogExtension(listener_.get(), NULL);
     93 
     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_);
    101 
    102     // Must run after our setup because it actually runs the test.
    103     ExtensionBrowserTest::SetUp();
    104   }
    105 
    106   virtual void TearDown() OVERRIDE {
    107     ExtensionBrowserTest::TearDown();
    108 
    109     // Delete the dialog first, as it holds a pointer to the listener.
    110     dialog_ = NULL;
    111     listener_.reset();
    112 
    113     second_dialog_ = NULL;
    114     second_listener_.reset();
    115   }
    116 
    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_);
    123   }
    124 
    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);
    133   }
    134 
    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 */);
    143 
    144     scoped_ptr<ExtensionTestMessageListener> additional_listener;
    145     if (!additional_message.empty()) {
    146       additional_listener.reset(
    147           new ExtensionTestMessageListener(additional_message, false));
    148     }
    149 
    150     dialog_->SelectFile(dialog_type,
    151                         base::string16() /* title */,
    152                         file_path,
    153                         NULL /* file_types */,
    154                          0 /* file_type_index */,
    155                         FILE_PATH_LITERAL("") /* default_extension */,
    156                         owning_window,
    157                         this /* params */);
    158 
    159     LOG(INFO) << "Waiting for JavaScript ready message.";
    160     ASSERT_TRUE(init_listener.WaitUntilSatisfied());
    161 
    162     if (additional_listener.get()) {
    163       LOG(INFO) << "Waiting for JavaScript " << additional_message
    164                 << " message.";
    165       ASSERT_TRUE(additional_listener->WaitUntilSatisfied());
    166     }
    167 
    168     // Dialog should be running now.
    169     ASSERT_TRUE(dialog_->IsRunning(owning_window));
    170 
    171     ASSERT_NO_FATAL_FAILURE(CheckJavascriptErrors());
    172   }
    173 
    174   void TryOpeningSecondDialog(const gfx::NativeWindow& owning_window) {
    175     second_listener_.reset(new MockSelectFileDialogListener());
    176     second_dialog_ = new SelectFileDialogExtension(second_listener_.get(),
    177                                                    NULL);
    178 
    179     // At the moment we don't really care about dialog type, but we have to put
    180     // some dialog type.
    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 */,
    187                                owning_window,
    188                                this /* params */);
    189   }
    190 
    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();
    210 
    211     // Dialog no longer believes it is running.
    212     ASSERT_FALSE(dialog_->IsRunning(owning_window));
    213   }
    214 
    215   scoped_ptr<MockSelectFileDialogListener> listener_;
    216   scoped_refptr<SelectFileDialogExtension> dialog_;
    217 
    218   scoped_ptr<MockSelectFileDialogListener> second_listener_;
    219   scoped_refptr<SelectFileDialogExtension> second_dialog_;
    220 
    221   base::ScopedTempDir tmp_dir_;
    222   base::FilePath downloads_dir_;
    223 };
    224 
    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);
    229 
    230   // Before we call SelectFile, dialog is not running/visible.
    231   ASSERT_FALSE(dialog_->IsRunning(native_window));
    232 }
    233 
    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();
    238   listener_.reset();
    239 }
    240 
    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.
    243 // crbug.com/98791
    244 
    245 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
    246                        SelectFileAndCancel) {
    247   AddMountPoint(downloads_dir_);
    248 
    249   gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
    250 
    251   // base::FilePath() for default path.
    252   ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
    253                                      base::FilePath(), owning_window, ""));
    254 
    255   // Press cancel button.
    256   CloseDialog(DIALOG_BTN_CANCEL, owning_window);
    257 
    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());
    262 }
    263 
    264 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
    265                        SelectFileAndOpen) {
    266   AddMountPoint(downloads_dir_);
    267 
    268   base::FilePath test_file =
    269       downloads_dir_.AppendASCII("file_manager_test.html");
    270 
    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));
    275 
    276   gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
    277 
    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"));
    286 
    287   // Click open button.
    288   CloseDialog(DIALOG_BTN_OK, owning_window);
    289 
    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());
    295 }
    296 
    297 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
    298                        SelectFileAndSave) {
    299   AddMountPoint(downloads_dir_);
    300 
    301   base::FilePath test_file =
    302       downloads_dir_.AppendASCII("file_manager_test.html");
    303 
    304   gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
    305 
    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"));
    314 
    315   // Click save button.
    316   CloseDialog(DIALOG_BTN_OK, owning_window);
    317 
    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());
    323 }
    324 
    325 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
    326                        OpenSingletonTabAndCancel) {
    327   AddMountPoint(downloads_dir_);
    328 
    329   gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
    330 
    331   ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
    332                                      base::FilePath(), owning_window, ""));
    333 
    334   // Open a singleton tab in background.
    335   chrome::NavigateParams p(browser(), GURL("http://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);
    340 
    341   // Press cancel button.
    342   CloseDialog(DIALOG_BTN_CANCEL, owning_window);
    343 
    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());
    348 }
    349 
    350 IN_PROC_BROWSER_TEST_F(SelectFileDialogExtensionBrowserTest,
    351                        OpenTwoDialogs) {
    352   AddMountPoint(downloads_dir_);
    353 
    354   gfx::NativeWindow owning_window = browser()->window()->GetNativeWindow();
    355 
    356   ASSERT_NO_FATAL_FAILURE(OpenDialog(ui::SelectFileDialog::SELECT_OPEN_FILE,
    357                                      base::FilePath(), owning_window, ""));
    358 
    359   TryOpeningSecondDialog(owning_window);
    360 
    361   // Second dialog should not be running.
    362   ASSERT_FALSE(second_dialog_->IsRunning(owning_window));
    363 
    364   // Click cancel button.
    365   CloseDialog(DIALOG_BTN_CANCEL, owning_window);
    366 
    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());
    371 }
    372