blob: 534640b824b210acc00076ee84f52cd8075e6b71 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/shell_dialogs/select_file_dialog_mac.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "base/apple/foundation_util.h"
#include "base/files/file_util.h"
#include "base/functional/callback_forward.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/ranges/algorithm.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/task_environment.h"
#include "components/remote_cocoa/app_shim/select_file_dialog_bridge.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "testing/platform_test.h"
#include "ui/shell_dialogs/select_file_policy.h"
#define EXPECT_EQ_BOOL(a, b) \
EXPECT_EQ(static_cast<bool>(a), static_cast<bool>(b))
namespace {
const int kFileTypePopupTag = 1234;
// Returns a vector containing extension descriptions for a given popup.
std::vector<std::u16string> GetExtensionDescriptionList(NSPopUpButton* popup) {
std::vector<std::u16string> extension_descriptions;
for (NSString* description in popup.itemTitles) {
extension_descriptions.push_back(base::SysNSStringToUTF16(description));
}
return extension_descriptions;
}
// Returns the NSPopupButton associated with the given `panel`.
NSPopUpButton* GetPopup(NSSavePanel* panel) {
return [panel.accessoryView viewWithTag:kFileTypePopupTag];
}
} // namespace
namespace ui::test {
// Helper test base to initialize SelectFileDialogImpl.
class SelectFileDialogMacTest : public PlatformTest,
public SelectFileDialog::Listener {
public:
SelectFileDialogMacTest()
: dialog_(new SelectFileDialogImpl(this, nullptr)) {}
SelectFileDialogMacTest(const SelectFileDialogMacTest&) = delete;
SelectFileDialogMacTest& operator=(const SelectFileDialogMacTest&) = delete;
// Overridden from SelectFileDialog::Listener.
void FileSelected(const SelectedFileInfo& file,
int index,
void* params) override {}
void FileSelectionCanceled(void* params) override {}
protected:
base::test::TaskEnvironment task_environment_ = base::test::TaskEnvironment(
base::test::TaskEnvironment::MainThreadType::UI);
struct FileDialogArguments {
SelectFileDialog::Type type = SelectFileDialog::SELECT_SAVEAS_FILE;
std::u16string title;
base::FilePath default_path;
raw_ptr<SelectFileDialog::FileTypeInfo> file_types = nullptr;
int file_type_index = 0;
base::FilePath::StringType default_extension;
raw_ptr<void> params = nullptr;
};
// Helper method to create a dialog with the given `args`. Returns the created
// NSSavePanel.
NSSavePanel* SelectFileWithParams(FileDialogArguments args) {
NSWindow* parent_window =
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
styleMask:NSWindowStyleMaskTitled
backing:NSBackingStoreBuffered
defer:NO];
parent_window.releasedWhenClosed = NO;
parent_windows_.push_back(parent_window);
dialog_->SelectFile(args.type, args.title, args.default_path,
args.file_types, args.file_type_index,
args.default_extension, parent_window, args.params);
// At this point, the Mojo IPC to show the dialog is queued up. Spin the
// message loop to get the Mojo IPC to happen.
base::RunLoop().RunUntilIdle();
// Now there is an actual panel that exists.
NSSavePanel* panel = remote_cocoa::SelectFileDialogBridge::
GetLastCreatedNativePanelForTesting();
DCHECK(panel);
// Pump the message loop until the panel reports that it's visible.
base::RunLoop run_loop;
base::RepeatingClosure quit_closure = run_loop.QuitClosure();
id<NSObject> observer = [NSNotificationCenter.defaultCenter
addObserverForName:NSWindowDidUpdateNotification
object:panel
queue:nil
usingBlock:^(NSNotification* note) {
if (panel.visible) {
quit_closure.Run();
}
}];
run_loop.Run();
[NSNotificationCenter.defaultCenter removeObserver:observer];
return panel;
}
// Returns the number of panels currently active.
size_t GetActivePanelCount() const {
return dialog_->dialog_data_list_.size();
}
// Sets a callback to be called when a dialog is closed.
void SetDialogClosedCallback(base::RepeatingClosure callback) {
dialog_->dialog_closed_callback_for_testing_ = callback;
}
void ResetDialog() {
dialog_ = new SelectFileDialogImpl(this, nullptr);
// Spin the run loop to get any pending Mojo IPC sent.
base::RunLoop().RunUntilIdle();
}
private:
scoped_refptr<SelectFileDialogImpl> dialog_;
std::vector<NSWindow*> parent_windows_;
};
class SelectFileDialogMacOpenAndSaveTest
: public SelectFileDialogMacTest,
public ::testing::WithParamInterface<SelectFileDialog::Type> {};
INSTANTIATE_TEST_SUITE_P(All,
SelectFileDialogMacOpenAndSaveTest,
::testing::Values(SelectFileDialog::SELECT_SAVEAS_FILE,
SelectFileDialog::SELECT_OPEN_FILE));
// Verify that the extension popup has the correct description and changing the
// popup item changes the allowed file types.
TEST_F(SelectFileDialogMacTest, ExtensionPopup) {
SelectFileDialog::FileTypeInfo file_type_info;
file_type_info.extensions = {{"html", "htm"}, {"jpeg", "jpg"}};
file_type_info.extension_description_overrides = {u"Webpage", u"Image"};
file_type_info.include_all_files = false;
FileDialogArguments args;
args.file_types = &file_type_info;
NSSavePanel* panel = SelectFileWithParams(args);
NSPopUpButton* popup = GetPopup(panel);
ASSERT_TRUE(popup);
// Check that the dropdown list created has the correct description.
const std::vector<std::u16string> extension_descriptions =
GetExtensionDescriptionList(popup);
EXPECT_EQ(file_type_info.extension_description_overrides,
extension_descriptions);
// Ensure other file types are not allowed.
EXPECT_FALSE(panel.allowsOtherFileTypes);
// Check that the first item was selected, since a file_type_index of 0 was
// passed and no default extension was provided.
EXPECT_EQ(0, popup.indexOfSelectedItem);
if (@available(macOS 11, *)) {
ASSERT_EQ(1lu, panel.allowedContentTypes.count);
EXPECT_NSEQ(UTTypeHTML, panel.allowedContentTypes[0]);
} else {
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"htm"]);
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"html"]);
// Extensions should appear in order of input.
EXPECT_LT([panel.allowedFileTypes indexOfObject:@"html"],
[panel.allowedFileTypes indexOfObject:@"htm"]);
EXPECT_FALSE([panel.allowedFileTypes containsObject:@"jpg"]);
}
// Select the second item.
[popup.menu performActionForItemAtIndex:1];
EXPECT_EQ(1, popup.indexOfSelectedItem);
if (@available(macOS 11, *)) {
ASSERT_EQ(1lu, panel.allowedContentTypes.count);
EXPECT_NSEQ(UTTypeJPEG, panel.allowedContentTypes[0]);
} else {
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"jpg"]);
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"jpeg"]);
// Extensions should appear in order of input.
EXPECT_LT([panel.allowedFileTypes indexOfObject:@"jpeg"],
[panel.allowedFileTypes indexOfObject:@"jpg"]);
EXPECT_FALSE([panel.allowedFileTypes containsObject:@"html"]);
}
}
// Verify file_type_info.include_all_files argument is respected.
TEST_P(SelectFileDialogMacOpenAndSaveTest, IncludeAllFiles) {
SelectFileDialog::FileTypeInfo file_type_info;
file_type_info.extensions = {{"html", "htm"}, {"jpeg", "jpg"}};
file_type_info.extension_description_overrides = {u"Webpage", u"Image"};
file_type_info.include_all_files = true;
FileDialogArguments args;
args.type = GetParam();
args.file_types = &file_type_info;
NSSavePanel* panel = SelectFileWithParams(args);
NSPopUpButton* popup = GetPopup(panel);
ASSERT_TRUE(popup);
// Ensure other file types are allowed.
EXPECT_TRUE(panel.allowsOtherFileTypes);
// Check that the dropdown list created has the correct description.
const std::vector<std::u16string> extension_descriptions =
GetExtensionDescriptionList(popup);
// Save dialogs don't have "all files".
if (args.type == SelectFileDialog::SELECT_SAVEAS_FILE) {
ASSERT_EQ(2lu, extension_descriptions.size());
EXPECT_EQ(u"Webpage", extension_descriptions[0]);
EXPECT_EQ(u"Image", extension_descriptions[1]);
} else {
ASSERT_EQ(3lu, extension_descriptions.size());
EXPECT_EQ(u"Webpage", extension_descriptions[0]);
EXPECT_EQ(u"Image", extension_descriptions[1]);
EXPECT_EQ(u"All Files", extension_descriptions[2]);
// Note that no further testing on the popup can be done. Open dialogs are
// out-of-process starting in macOS 10.15, so once it's been run and closed,
// the accessory view controls no longer work.
}
}
// Verify that file_type_index and default_extension arguments cause the
// appropriate extension group to be initially selected.
TEST_F(SelectFileDialogMacTest, InitialSelection) {
SelectFileDialog::FileTypeInfo file_type_info;
file_type_info.extensions = {{"html", "htm"}, {"jpeg", "jpg"}};
file_type_info.extension_description_overrides = {u"Webpage", u"Image"};
FileDialogArguments args;
args.file_types = &file_type_info;
args.file_type_index = 2;
args.default_extension = "jpg";
NSSavePanel* panel = SelectFileWithParams(args);
NSPopUpButton* popup = GetPopup(panel);
ASSERT_TRUE(popup);
// Verify that the `file_type_index` causes the second item to be initially
// selected.
EXPECT_EQ(1, popup.indexOfSelectedItem);
if (@available(macOS 11, *)) {
ASSERT_EQ(1lu, panel.allowedContentTypes.count);
EXPECT_NSEQ(UTTypeJPEG, panel.allowedContentTypes[0]);
} else {
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"jpg"]);
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"jpeg"]);
EXPECT_FALSE([panel.allowedFileTypes containsObject:@"html"]);
}
ResetDialog();
args.file_type_index = 0;
args.default_extension = "pdf";
panel = SelectFileWithParams(args);
popup = GetPopup(panel);
ASSERT_TRUE(popup);
// Verify that the first item was selected, since the default extension passed
// was not present in the extension list.
EXPECT_EQ(0, popup.indexOfSelectedItem);
if (@available(macOS 11, *)) {
ASSERT_EQ(1lu, panel.allowedContentTypes.count);
EXPECT_NSEQ(UTTypeHTML, panel.allowedContentTypes[0]);
} else {
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"html"]);
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"htm"]);
EXPECT_FALSE([panel.allowedFileTypes containsObject:@"pdf"]);
EXPECT_FALSE([panel.allowedFileTypes containsObject:@"jpeg"]);
}
ResetDialog();
args.file_type_index = 0;
args.default_extension = "jpg";
panel = SelectFileWithParams(args);
popup = GetPopup(panel);
ASSERT_TRUE(popup);
// Verify that the extension group corresponding to the default extension is
// initially selected.
EXPECT_EQ(1, popup.indexOfSelectedItem);
if (@available(macOS 11, *)) {
ASSERT_EQ(1lu, panel.allowedContentTypes.count);
EXPECT_NSEQ(UTTypeJPEG, panel.allowedContentTypes[0]);
} else {
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"jpg"]);
EXPECT_TRUE([panel.allowedFileTypes containsObject:@"jpeg"]);
EXPECT_FALSE([panel.allowedFileTypes containsObject:@"html"]);
}
}
// Verify that an appropriate extension description is shown even if an empty
// extension description is passed for a given extension group.
TEST_F(SelectFileDialogMacTest, EmptyDescription) {
SelectFileDialog::FileTypeInfo file_type_info;
file_type_info.extensions = {{"pdf"}, {"jpg"}, {"qqq"}};
file_type_info.extension_description_overrides = {u"", u"Image", u""};
FileDialogArguments args;
args.file_types = &file_type_info;
NSSavePanel* panel = SelectFileWithParams(args);
NSPopUpButton* popup = GetPopup(panel);
ASSERT_TRUE(popup);
const std::vector<std::u16string> extension_descriptions =
GetExtensionDescriptionList(popup);
ASSERT_EQ(3lu, extension_descriptions.size());
// Verify that the correct system description is produced for known file types
// like pdf if no extension description is provided by the client. Modern
// versions of macOS have settled on "PDF document" as the official
// description.
EXPECT_EQ(u"PDF document", extension_descriptions[0]);
// Verify that if an override description is provided, it is used.
EXPECT_EQ(u"Image", extension_descriptions[1]);
// Verify the description for unknown file types if no extension description
// is provided by the client.
EXPECT_EQ(u"QQQ File (.qqq)", extension_descriptions[2]);
}
// Verify that passing an empty extension list in file_type_info causes no
// extension dropdown to display.
TEST_P(SelectFileDialogMacOpenAndSaveTest, EmptyExtension) {
SelectFileDialog::FileTypeInfo file_type_info;
FileDialogArguments args;
args.type = GetParam();
args.file_types = &file_type_info;
NSSavePanel* panel = SelectFileWithParams(args);
EXPECT_FALSE(panel.accessoryView);
// Ensure other file types are allowed.
EXPECT_TRUE(panel.allowsOtherFileTypes);
}
// Verify that passing a null file_types value causes no extension dropdown to
// display.
TEST_F(SelectFileDialogMacTest, FileTypesNull) {
NSSavePanel* panel = SelectFileWithParams({});
EXPECT_TRUE(panel.allowsOtherFileTypes);
EXPECT_FALSE(panel.accessoryView);
// Ensure other file types are allowed.
EXPECT_TRUE(panel.allowsOtherFileTypes);
}
// Verify that appropriate properties are set on the NSSavePanel for different
// dialog types.
TEST_F(SelectFileDialogMacTest, SelectionType) {
SelectFileDialog::FileTypeInfo file_type_info;
FileDialogArguments args;
args.file_types = &file_type_info;
enum {
HAS_ACCESSORY_VIEW = 1,
PICK_FILES = 2,
PICK_DIRS = 4,
CREATE_DIRS = 8,
MULTIPLE_SELECTION = 16,
};
struct SelectionTypeTestCase {
SelectFileDialog::Type type;
unsigned options;
std::string prompt;
} test_cases[] = {
{SelectFileDialog::SELECT_FOLDER, PICK_DIRS | CREATE_DIRS, "Select"},
{SelectFileDialog::SELECT_UPLOAD_FOLDER, PICK_DIRS, "Upload"},
{SelectFileDialog::SELECT_EXISTING_FOLDER, PICK_DIRS, "Select"},
{SelectFileDialog::SELECT_SAVEAS_FILE, CREATE_DIRS, "Save"},
{SelectFileDialog::SELECT_OPEN_FILE, PICK_FILES, "Open"},
{SelectFileDialog::SELECT_OPEN_MULTI_FILE,
PICK_FILES | MULTIPLE_SELECTION, "Open"},
};
for (size_t i = 0; i < std::size(test_cases); i++) {
SCOPED_TRACE(
base::StringPrintf("i=%lu file_dialog_type=%d", i, test_cases[i].type));
args.type = test_cases[i].type;
ResetDialog();
NSSavePanel* panel = SelectFileWithParams(args);
EXPECT_EQ_BOOL(test_cases[i].options & HAS_ACCESSORY_VIEW,
panel.accessoryView);
EXPECT_EQ_BOOL(test_cases[i].options & CREATE_DIRS,
panel.canCreateDirectories);
EXPECT_EQ(test_cases[i].prompt, base::SysNSStringToUTF8([panel prompt]));
if (args.type != SelectFileDialog::SELECT_SAVEAS_FILE) {
NSOpenPanel* open_panel = base::apple::ObjCCast<NSOpenPanel>(panel);
// Verify that for types other than save file dialogs, an NSOpenPanel is
// created.
ASSERT_TRUE(open_panel);
EXPECT_EQ_BOOL(test_cases[i].options & PICK_FILES,
open_panel.canChooseFiles);
EXPECT_EQ_BOOL(test_cases[i].options & PICK_DIRS,
open_panel.canChooseDirectories);
EXPECT_EQ_BOOL(test_cases[i].options & MULTIPLE_SELECTION,
open_panel.allowsMultipleSelection);
}
}
}
// Verify that the correct message is set on the NSSavePanel.
TEST_F(SelectFileDialogMacTest, DialogMessage) {
const std::string test_title = "test title";
FileDialogArguments args;
args.title = base::ASCIIToUTF16(test_title);
NSSavePanel* panel = SelectFileWithParams(args);
EXPECT_EQ(test_title, base::SysNSStringToUTF8(panel.message));
}
// Verify that multiple file dialogs are correctly handled.
TEST_F(SelectFileDialogMacTest, MultipleDialogs) {
FileDialogArguments args;
NSSavePanel* panel1 = SelectFileWithParams(args);
NSSavePanel* panel2 = SelectFileWithParams(args);
ASSERT_EQ(2lu, GetActivePanelCount());
// Verify closing the panel decreases the panel count.
base::RunLoop run_loop1;
SetDialogClosedCallback(run_loop1.QuitClosure());
[panel1 cancel:nil];
run_loop1.Run();
ASSERT_EQ(1lu, GetActivePanelCount());
// In 10.15, file picker dialogs are remote, and the restriction of apps not
// being allowed to OK their own file requests has been extended from just
// sandboxed apps to all apps. Since the dialogs can't be OKed, at least close
// them all.
base::RunLoop run_loop2;
SetDialogClosedCallback(run_loop2.QuitClosure());
[panel2 cancel:nil];
run_loop2.Run();
EXPECT_EQ(0lu, GetActivePanelCount());
SetDialogClosedCallback({});
}
// Verify that the default_path argument is respected.
TEST_F(SelectFileDialogMacTest, DefaultPath) {
FileDialogArguments args;
args.default_path = base::GetHomeDir().AppendASCII("test.txt");
NSSavePanel* panel = SelectFileWithParams(args);
panel.extensionHidden = NO;
EXPECT_EQ(args.default_path.DirName(),
base::apple::NSStringToFilePath(panel.directoryURL.path));
EXPECT_EQ(args.default_path.BaseName(),
base::apple::NSStringToFilePath(panel.nameFieldStringValue));
}
// Verify that the file dialog does not hide extension for filenames with
// multiple extensions.
TEST_F(SelectFileDialogMacTest, MultipleExtension) {
const std::string fake_path_normal = "/fake_directory/filename.tar";
const std::string fake_path_multiple = "/fake_directory/filename.tar.gz";
const std::string fake_path_long = "/fake_directory/example.com-123.json";
FileDialogArguments args;
args.default_path = base::FilePath(FILE_PATH_LITERAL(fake_path_normal));
NSSavePanel* panel = SelectFileWithParams(args);
EXPECT_TRUE(panel.canSelectHiddenExtension);
EXPECT_TRUE(panel.extensionHidden);
ResetDialog();
args.default_path = base::FilePath(FILE_PATH_LITERAL(fake_path_multiple));
panel = SelectFileWithParams(args);
EXPECT_FALSE(panel.canSelectHiddenExtension);
EXPECT_FALSE(panel.extensionHidden);
ResetDialog();
args.default_path = base::FilePath(FILE_PATH_LITERAL(fake_path_long));
panel = SelectFileWithParams(args);
EXPECT_FALSE(panel.canSelectHiddenExtension);
EXPECT_FALSE(panel.extensionHidden);
}
// Verify that the file dialog does not hide extension when the
// `keep_extension_visible` flag is set to true.
TEST_F(SelectFileDialogMacTest, KeepExtensionVisible) {
SelectFileDialog::FileTypeInfo file_type_info;
file_type_info.extensions = {{"html", "htm"}, {"jpeg", "jpg"}};
file_type_info.keep_extension_visible = true;
FileDialogArguments args;
args.file_types = &file_type_info;
NSSavePanel* panel = SelectFileWithParams(args);
EXPECT_FALSE(panel.canSelectHiddenExtension);
EXPECT_FALSE(panel.extensionHidden);
}
// TODO(crbug.com/1427906): This has been flaky.
TEST_F(SelectFileDialogMacTest, DISABLED_DontCrashWithBogusExtension) {
SelectFileDialog::FileTypeInfo file_type_info;
file_type_info.extensions = {{"bogus type", "j.pg"}};
FileDialogArguments args;
args.file_types = &file_type_info;
NSSavePanel* panel = SelectFileWithParams(args);
// If execution gets this far, there was no crash.
EXPECT_TRUE(panel);
}
} // namespace ui::test