123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- // Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- #include "ui/shell_dialogs/select_file_dialog_win.h"
- #include <stddef.h>
- #include <memory>
- #include <string>
- #include <vector>
- #include "base/files/file_path.h"
- #include "base/files/file_util.h"
- #include "base/files/scoped_temp_dir.h"
- #include "base/logging.h"
- #include "base/memory/scoped_refptr.h"
- #include "base/strings/stringprintf.h"
- #include "base/strings/utf_string_conversions.h"
- #include "base/test/task_environment.h"
- #include "base/test/test_timeouts.h"
- #include "base/threading/platform_thread.h"
- #include "base/win/scoped_com_initializer.h"
- #include "base/win/windows_version.h"
- #include "testing/gtest/include/gtest/gtest.h"
- #include "ui/base/l10n/l10n_util.h"
- #include "ui/shell_dialogs/select_file_dialog.h"
- #include "ui/shell_dialogs/select_file_policy.h"
- #include "ui/strings/grit/ui_strings.h"
- namespace {
- // The default title for the various dialogs.
- constexpr wchar_t kSelectFolderDefaultTitle[] = L"Select Folder";
- constexpr wchar_t kSelectFileDefaultTitle[] = L"Open";
- constexpr wchar_t kSaveFileDefaultTitle[] = L"Save As";
- // Returns the title of |window|.
- std::wstring GetWindowTitle(HWND window) {
- wchar_t buffer[256];
- UINT count = ::GetWindowText(window, buffer, std::size(buffer));
- return std::wstring(buffer, count);
- }
- // Waits for a dialog window whose title is |dialog_title| to show and returns
- // its handle.
- HWND WaitForDialogWindow(const std::wstring& dialog_title) {
- // File dialogs uses this class name.
- static constexpr wchar_t kDialogClassName[] = L"#32770";
- HWND result = nullptr;
- base::TimeDelta max_wait_time = TestTimeouts::action_timeout();
- base::TimeDelta retry_interval = base::Milliseconds(20);
- while (!result && (max_wait_time.InMilliseconds() > 0)) {
- result = ::FindWindow(kDialogClassName, dialog_title.c_str());
- base::PlatformThread::Sleep(retry_interval);
- max_wait_time -= retry_interval;
- }
- if (!result) {
- LOG(ERROR) << "Wait for dialog window timed out.";
- }
- // Check the name of the dialog specifically. That's because if multiple file
- // dialogs are opened in quick successions (e.g. from one test to another),
- // the ::FindWindow() call above will work for both the previous dialog title
- // and the current.
- return GetWindowTitle(result) == dialog_title ? result : nullptr;
- }
- struct EnumWindowsParam {
- // The owner of the dialog. This is used to differentiate the dialog prompt
- // from the file dialog since they could have the same title.
- HWND owner;
- // Holds the resulting window.
- HWND result;
- };
- BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM param) {
- EnumWindowsParam* enum_param = reinterpret_cast<EnumWindowsParam*>(param);
- // Early continue if the current hwnd is the file dialog.
- if (hwnd == enum_param->owner)
- return TRUE;
- // Only consider visible windows.
- if (!::IsWindowVisible(hwnd))
- return TRUE;
- // If the window doesn't have |enum_param->owner| as the owner, it can't be
- // the prompt dialog.
- if (::GetWindow(hwnd, GW_OWNER) != enum_param->owner)
- return TRUE;
- enum_param->result = hwnd;
- return FALSE;
- }
- HWND WaitForDialogPrompt(HWND owner) {
- // The dialog prompt could have the same title as the file dialog. This means
- // that it would not be possible to make sure the right window is found using
- // ::FindWindow(). Instead enumerate all top-level windows and return the one
- // whose owner is the file dialog.
- EnumWindowsParam param = {owner, nullptr};
- base::TimeDelta max_wait_time = TestTimeouts::action_timeout();
- base::TimeDelta retry_interval = base::Milliseconds(20);
- while (!param.result && (max_wait_time.InMilliseconds() > 0)) {
- ::EnumWindows(&EnumWindowsCallback, reinterpret_cast<LPARAM>(¶m));
- base::PlatformThread::Sleep(retry_interval);
- max_wait_time -= retry_interval;
- }
- if (!param.result) {
- LOG(ERROR) << "Wait for dialog prompt timed out.";
- }
- return param.result;
- }
- // Returns the text of the dialog item in |window| whose id is |dialog_item_id|.
- std::wstring GetDialogItemText(HWND window, int dialog_item_id) {
- if (!window)
- return std::wstring();
- wchar_t buffer[256];
- UINT count =
- ::GetDlgItemText(window, dialog_item_id, buffer, std::size(buffer));
- return std::wstring(buffer, count);
- }
- // Sends a command to |window| using PostMessage().
- void SendCommand(HWND window, int id) {
- ASSERT_TRUE(window);
- // Make sure the window is visible first or the WM_COMMAND may not have any
- // effect.
- base::TimeDelta max_wait_time = TestTimeouts::action_timeout();
- base::TimeDelta retry_interval = base::Milliseconds(20);
- while (!::IsWindowVisible(window) && (max_wait_time.InMilliseconds() > 0)) {
- base::PlatformThread::Sleep(retry_interval);
- max_wait_time -= retry_interval;
- }
- if (!::IsWindowVisible(window)) {
- LOG(ERROR) << "SendCommand timed out.";
- }
- ::PostMessage(window, WM_COMMAND, id, 0);
- }
- } // namespace
- class SelectFileDialogWinTest : public ::testing::Test,
- public ui::SelectFileDialog::Listener {
- public:
- SelectFileDialogWinTest() = default;
- SelectFileDialogWinTest(const SelectFileDialogWinTest&) = delete;
- SelectFileDialogWinTest& operator=(const SelectFileDialogWinTest&) = delete;
- ~SelectFileDialogWinTest() override = default;
- // ui::SelectFileDialog::Listener:
- void FileSelected(const base::FilePath& path,
- int index,
- void* params) override {
- selected_paths_.push_back(path);
- }
- void MultiFilesSelected(const std::vector<base::FilePath>& files,
- void* params) override {
- selected_paths_ = files;
- }
- void FileSelectionCanceled(void* params) override { was_cancelled_ = true; }
- // Runs the scheduler until no tasks are executing anymore.
- void RunUntilIdle() { task_environment_.RunUntilIdle(); }
- const std::vector<base::FilePath>& selected_paths() {
- return selected_paths_;
- }
- // Return a fake NativeWindow. This will result in the dialog having no
- // parent window but the tests will still work.
- static gfx::NativeWindow native_window() {
- return reinterpret_cast<gfx::NativeWindow>(0);
- }
- bool was_cancelled() { return was_cancelled_; }
- // Resets the results so that this instance can be reused as a
- // SelectFileDialog listener.
- void ResetResults() {
- was_cancelled_ = false;
- selected_paths_.clear();
- }
- private:
- base::test::TaskEnvironment task_environment_;
- std::vector<base::FilePath> selected_paths_;
- bool was_cancelled_ = false;
- };
- TEST_F(SelectFileDialogWinTest, CancelAllDialogs) {
- // TODO(crbug.com/1265379): Flaky on Windows 7.
- if (base::win::GetVersion() <= base::win::Version::WIN7)
- GTEST_SKIP() << "Skipping test for Windows 7";
- // Intentionally not testing SELECT_UPLOAD_FOLDER because the dialog is
- // customized for that case.
- struct {
- ui::SelectFileDialog::Type dialog_type;
- const wchar_t* dialog_title;
- } kTestCases[] = {
- {
- ui::SelectFileDialog::SELECT_FOLDER, kSelectFolderDefaultTitle,
- },
- {
- ui::SelectFileDialog::SELECT_EXISTING_FOLDER,
- kSelectFolderDefaultTitle,
- },
- {
- ui::SelectFileDialog::SELECT_SAVEAS_FILE, kSaveFileDefaultTitle,
- },
- {
- ui::SelectFileDialog::SELECT_OPEN_FILE, kSelectFileDefaultTitle,
- },
- {
- ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE, kSelectFileDefaultTitle,
- }};
- for (size_t i = 0; i < std::size(kTestCases); ++i) {
- SCOPED_TRACE(base::StringPrintf("i=%zu", i));
- const auto& test_case = kTestCases[i];
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- std::unique_ptr<ui::SelectFileDialog::FileTypeInfo> file_type_info;
- int file_type_info_index = 0;
- // The Save As dialog requires a filetype info.
- if (test_case.dialog_type == ui::SelectFileDialog::SELECT_SAVEAS_FILE) {
- file_type_info = std::make_unique<ui::SelectFileDialog::FileTypeInfo>();
- file_type_info->extensions.push_back({L"html"});
- file_type_info_index = 1;
- }
- dialog->SelectFile(test_case.dialog_type, std::u16string(),
- base::FilePath(), file_type_info.get(),
- file_type_info_index, std::wstring(), native_window(),
- nullptr);
- // Accept the default value.
- HWND window = WaitForDialogWindow(test_case.dialog_title);
- SendCommand(window, IDCANCEL);
- RunUntilIdle();
- EXPECT_TRUE(was_cancelled());
- EXPECT_TRUE(selected_paths().empty());
- ResetResults();
- }
- }
- // When using SELECT_UPLOAD_FOLDER, the title and the ok button strings are
- // modified to put emphasis on the fact that the whole folder will be uploaded.
- TEST_F(SelectFileDialogWinTest, UploadFolderCheckStrings) {
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- base::FilePath default_path = scoped_temp_dir.GetPath();
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_UPLOAD_FOLDER,
- std::u16string(), default_path, nullptr, 0, L"",
- native_window(), nullptr);
- // Wait for the window to open and make sure the window title was changed from
- // the default title for a regular select folder operation.
- HWND window = WaitForDialogWindow(base::UTF16ToWide(
- l10n_util::GetStringUTF16(IDS_SELECT_UPLOAD_FOLDER_DIALOG_TITLE)));
- EXPECT_NE(GetWindowTitle(window), kSelectFolderDefaultTitle);
- // Check the OK button text.
- EXPECT_EQ(GetDialogItemText(window, 1),
- base::UTF16ToWide(l10n_util::GetStringUTF16(
- IDS_SELECT_UPLOAD_FOLDER_DIALOG_UPLOAD_BUTTON)));
- // Close the dialog.
- SendCommand(window, IDOK);
- RunUntilIdle();
- EXPECT_FALSE(was_cancelled());
- ASSERT_EQ(1u, selected_paths().size());
- EXPECT_EQ(selected_paths()[0], default_path);
- }
- // Specifying the title when opening a dialog to select a file, select multiple
- // files or save a file doesn't do anything.
- TEST_F(SelectFileDialogWinTest, SpecifyTitle) {
- static constexpr char16_t kTitle[] = u"FooBar Title";
- // Create some file in a test folder.
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- // Create an existing file since it is required.
- base::FilePath default_path = scoped_temp_dir.GetPath().Append(L"foo.txt");
- std::string contents = "Hello test!";
- ASSERT_TRUE(base::WriteFile(default_path, contents));
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE, kTitle,
- default_path, nullptr, 0, L"", native_window(), nullptr);
- // Wait for the window to open. The title is unchanged. Note that if this
- // hangs, it possibly is because the title changed.
- HWND window = WaitForDialogWindow(kSelectFileDefaultTitle);
- // Close the dialog and the result doesn't matter.
- SendCommand(window, IDCANCEL);
- }
- // Tests the selection of one file in both the single and multiple case. It's
- // too much trouble to select a different file in the dialog so the default_path
- // is used to pre-select a file and the OK button is clicked as soon as the
- // dialog opens. This tests the default_path parameter and the single file
- // selection.
- TEST_F(SelectFileDialogWinTest, TestSelectFile) {
- // Create some file in a test folder.
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- // Create an existing file since it is required.
- base::FilePath default_path = scoped_temp_dir.GetPath().Append(L"foo.txt");
- std::string contents = "Hello test!";
- ASSERT_TRUE(base::WriteFile(default_path, contents));
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE, std::u16string(),
- default_path, nullptr, 0, L"", native_window(), nullptr);
- // Wait for the window to open
- HWND window = WaitForDialogWindow(kSelectFileDefaultTitle);
- SendCommand(window, IDOK);
- RunUntilIdle();
- EXPECT_FALSE(was_cancelled());
- ASSERT_EQ(1u, selected_paths().size());
- EXPECT_EQ(selected_paths()[0], default_path);
- }
- // Tests that the file extension is automatically added.
- TEST_F(SelectFileDialogWinTest, TestSaveFile) {
- // Create some file in a test folder.
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- base::FilePath default_path = scoped_temp_dir.GetPath().Append(L"foo");
- ui::SelectFileDialog::FileTypeInfo file_type_info;
- file_type_info.extensions.push_back({L"html"});
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_SAVEAS_FILE, std::u16string(),
- default_path, &file_type_info, 1, L"", native_window(),
- nullptr);
- // Wait for the window to open
- HWND window = WaitForDialogWindow(kSaveFileDefaultTitle);
- SendCommand(window, IDOK);
- RunUntilIdle();
- EXPECT_FALSE(was_cancelled());
- ASSERT_EQ(1u, selected_paths().size());
- EXPECT_EQ(selected_paths()[0], default_path.AddExtension(L"html"));
- }
- // Tests that only specifying a basename as the default path works.
- TEST_F(SelectFileDialogWinTest, OnlyBasename) {
- base::FilePath default_path(L"foobar.html");
- ui::SelectFileDialog::FileTypeInfo file_type_info;
- file_type_info.extensions.push_back({L"html"});
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_SAVEAS_FILE, std::u16string(),
- default_path, &file_type_info, 1, L"", native_window(),
- nullptr);
- // Wait for the window to open
- HWND window = WaitForDialogWindow(kSaveFileDefaultTitle);
- SendCommand(window, IDOK);
- RunUntilIdle();
- EXPECT_FALSE(was_cancelled());
- ASSERT_EQ(1u, selected_paths().size());
- EXPECT_EQ(selected_paths()[0].BaseName(), default_path);
- }
- TEST_F(SelectFileDialogWinTest, SaveAsDifferentExtension) {
- // Create some file in a test folder.
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- base::FilePath default_path = scoped_temp_dir.GetPath().Append(L"foo.txt");
- ui::SelectFileDialog::FileTypeInfo file_type_info;
- file_type_info.extensions.push_back({L"exe"});
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_SAVEAS_FILE, std::u16string(),
- default_path, &file_type_info, 1, L"html", native_window(),
- nullptr);
- HWND window = WaitForDialogWindow(kSaveFileDefaultTitle);
- SendCommand(window, IDOK);
- RunUntilIdle();
- EXPECT_FALSE(was_cancelled());
- EXPECT_EQ(selected_paths()[0], default_path);
- }
- TEST_F(SelectFileDialogWinTest, OpenFileDifferentExtension) {
- // Create some file in a test folder.
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- base::FilePath default_path = scoped_temp_dir.GetPath().Append(L"foo.txt");
- std::string contents = "Hello test!";
- ASSERT_TRUE(base::WriteFile(default_path, contents));
- ui::SelectFileDialog::FileTypeInfo file_type_info;
- file_type_info.extensions.push_back({L"exe"});
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE, std::u16string(),
- default_path, &file_type_info, 1, L"html", native_window(),
- nullptr);
- HWND window = WaitForDialogWindow(kSelectFileDefaultTitle);
- SendCommand(window, IDOK);
- RunUntilIdle();
- EXPECT_FALSE(was_cancelled());
- EXPECT_EQ(selected_paths()[0], default_path);
- }
- TEST_F(SelectFileDialogWinTest, SelectNonExistingFile) {
- // Create some file in a test folder.
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- base::FilePath default_path =
- scoped_temp_dir.GetPath().Append(L"does-not-exist.html");
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_OPEN_FILE, std::u16string(),
- default_path, nullptr, 0, L"", native_window(), nullptr);
- HWND window = WaitForDialogWindow(kSelectFileDefaultTitle);
- SendCommand(window, IDOK);
- // Since selecting a non-existing file is not supported, a error dialog box
- // should have appeared.
- HWND error_box = WaitForDialogPrompt(window);
- SendCommand(error_box, IDOK);
- // Now actually cancel the file dialog box.
- SendCommand(window, IDCANCEL);
- RunUntilIdle();
- EXPECT_TRUE(was_cancelled());
- EXPECT_TRUE(selected_paths().empty());
- }
- // Tests that selecting an existing file when saving should prompt the user with
- // a dialog to confirm the overwrite.
- TEST_F(SelectFileDialogWinTest, SaveFileOverwritePrompt) {
- // Create some file in a test folder.
- base::ScopedTempDir scoped_temp_dir;
- ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
- base::FilePath default_path = scoped_temp_dir.GetPath().Append(L"foo.txt");
- std::string contents = "Hello test!";
- ASSERT_TRUE(base::WriteFile(default_path, contents));
- ui::SelectFileDialog::FileTypeInfo file_type_info;
- file_type_info.extensions.push_back({L"txt"});
- scoped_refptr<ui::SelectFileDialog> dialog =
- ui::SelectFileDialog::Create(this, nullptr);
- dialog->SelectFile(ui::SelectFileDialog::SELECT_SAVEAS_FILE, std::u16string(),
- default_path, &file_type_info, 1, L"", native_window(),
- nullptr);
- HWND window = WaitForDialogWindow(kSaveFileDefaultTitle);
- SendCommand(window, IDOK);
- // Check that the prompt appears and close it. By default, the "no" option is
- // selected so sending IDOK cancels the operation.
- HWND error_box = WaitForDialogPrompt(window);
- SendCommand(error_box, IDOK);
- // Cancel the dialog.
- SendCommand(window, IDCANCEL);
- RunUntilIdle();
- EXPECT_TRUE(was_cancelled());
- EXPECT_TRUE(selected_paths().empty());
- }
|