blob: 7e4737813b4c02d1318fa2915b155a3810eec0d2 [file] [log] [blame]
// Copyright 2016 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.
#import "ui/views/cocoa/drag_drop_client_mac.h"
#import <Cocoa/Cocoa.h>
#include "base/bind.h"
#include "base/mac/availability.h"
#import "base/mac/scoped_objc_class_swizzler.h"
#include "base/mac/sdk_forward_declarations.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#import "ui/base/clipboard/clipboard_util_mac.h"
#include "ui/gfx/image/image_unittest_util.h"
#import "ui/views/cocoa/bridged_native_widget_host_impl.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view.h"
#include "ui/views/widget/native_widget_mac.h"
#include "ui/views/widget/widget.h"
#import "ui/views_bridge_mac/bridged_native_widget_impl.h"
using base::ASCIIToUTF16;
@interface NSView (DragSessionTestingDonor)
@end
@implementation NSView (DragSessionTestingDonor)
- (NSDraggingSession*)cr_beginDraggingSessionWithItems:(NSArray*)items
event:(NSEvent*)event
source:(id<NSDraggingSource>)
source {
return nil;
}
@end
// Mocks the NSDraggingInfo sent to the DragDropClientMac's DragUpdate() and
// Drop() methods. Out of the required methods of the protocol, only
// draggingLocation and draggingPasteboard are used.
@interface MockDraggingInfo : NSObject<NSDraggingInfo> {
NSPasteboard* pasteboard_;
}
@property BOOL animatesToDestination;
@property NSInteger numberOfValidItemsForDrop;
@property NSDraggingFormation draggingFormation;
@property(readonly)
NSSpringLoadingHighlight springLoadingHighlight API_AVAILABLE(macos(10.11));
@end
@implementation MockDraggingInfo
@synthesize animatesToDestination;
@synthesize numberOfValidItemsForDrop;
@synthesize draggingFormation;
@synthesize springLoadingHighlight;
- (instancetype)initWithPasteboard:(NSPasteboard*)pasteboard {
if ((self = [super init])) {
pasteboard_ = pasteboard;
}
return self;
}
- (NSPoint)draggingLocation {
return NSMakePoint(50, 50);
}
- (NSPasteboard*)draggingPasteboard {
return pasteboard_;
}
- (NSInteger)draggingSequenceNumber {
return 0;
}
- (id)draggingSource {
return nil;
}
- (NSDragOperation)draggingSourceOperationMask {
return NSDragOperationEvery;
}
- (NSWindow*)draggingDestinationWindow {
return nil;
}
- (NSArray*)namesOfPromisedFilesDroppedAtDestination:(NSURL*)dropDestination {
return nil;
}
- (NSImage*)draggedImage {
return nil;
}
- (NSPoint)draggedImageLocation {
return NSZeroPoint;
}
- (void)slideDraggedImageTo:(NSPoint)aPoint {
}
- (void)
enumerateDraggingItemsWithOptions:(NSDraggingItemEnumerationOptions)enumOpts
forView:(NSView*)view
classes:(NSArray*)classArray
searchOptions:(NSDictionary*)searchOptions
usingBlock:(void (^)(NSDraggingItem* draggingItem,
NSInteger idx,
BOOL* stop))block {
}
- (void)resetSpringLoading {
}
@end
namespace views {
namespace test {
// View object that will receive and process dropped data from the test.
class DragDropView : public View {
public:
DragDropView() {}
void set_formats(int formats) { formats_ = formats; }
// View:
bool GetDropFormats(
int* formats,
std::set<ui::ClipboardFormatType>* format_types) override {
*formats |= formats_;
return true;
}
bool CanDrop(const OSExchangeData& data) override { return true; }
int OnDragUpdated(const ui::DropTargetEvent& event) override {
return ui::DragDropTypes::DRAG_COPY;
}
int OnPerformDrop(const ui::DropTargetEvent& event) override {
return ui::DragDropTypes::DRAG_MOVE;
}
private:
// Drop formats accepted by this View object.
int formats_ = 0;
DISALLOW_COPY_AND_ASSIGN(DragDropView);
};
class DragDropClientMacTest : public WidgetTest {
public:
DragDropClientMacTest() : widget_(new Widget) {}
DragDropClientMac* drag_drop_client() {
return bridge_host_->drag_drop_client();
}
NSDragOperation DragUpdate(NSPasteboard* pasteboard) {
DragDropClientMac* client = drag_drop_client();
dragging_info_.reset(
[[MockDraggingInfo alloc] initWithPasteboard:pasteboard]);
return client->DragUpdate(dragging_info_.get());
}
NSDragOperation Drop() {
DragDropClientMac* client = drag_drop_client();
DCHECK(dragging_info_.get());
NSDragOperation operation = client->Drop(dragging_info_.get());
dragging_info_.reset();
return operation;
}
void SetData(OSExchangeData& data) {
drag_drop_client()->data_source_.reset(
[[CocoaDragDropDataProvider alloc] initWithData:data]);
}
// testing::Test:
void SetUp() override {
WidgetTest::SetUp();
widget_ = CreateTopLevelPlatformWidget();
gfx::Rect bounds(0, 0, 100, 100);
widget_->SetBounds(bounds);
bridge_host_ = BridgedNativeWidgetHostImpl::GetFromNativeWindow(
widget_->GetNativeWindow());
bridge_ = bridge_host_->bridge_impl();
widget_->Show();
target_ = new DragDropView();
widget_->GetContentsView()->AddChildView(target_);
target_->SetBoundsRect(bounds);
drag_drop_client()->operation_ = ui::DragDropTypes::DRAG_COPY;
}
void TearDown() override {
if (widget_)
widget_->CloseNow();
WidgetTest::TearDown();
}
protected:
Widget* widget_ = nullptr;
BridgedNativeWidgetImpl* bridge_ = nullptr;
BridgedNativeWidgetHostImpl* bridge_host_ = nullptr;
DragDropView* target_ = nullptr;
base::scoped_nsobject<MockDraggingInfo> dragging_info_;
private:
DISALLOW_COPY_AND_ASSIGN(DragDropClientMacTest);
};
// Tests if the drag and drop target receives the dropped data.
TEST_F(DragDropClientMacTest, BasicDragDrop) {
// Create the drop data
OSExchangeData data;
const base::string16& text = ASCIIToUTF16("text");
data.SetString(text);
SetData(data);
target_->set_formats(ui::OSExchangeData::STRING | ui::OSExchangeData::URL);
// Check if the target receives the data from a drop and returns the expected
// operation.
EXPECT_EQ(DragUpdate(nil), NSDragOperationCopy);
EXPECT_EQ(Drop(), NSDragOperationMove);
}
// Ensure that capture is released before the end of a drag and drop operation.
TEST_F(DragDropClientMacTest, ReleaseCapture) {
// DragDropView doesn't actually capture the mouse, so explicitly acquire it
// to test that StartDragAndDrop() actually releases it.
// Although this is not an interactive UI test, acquiring capture should be OK
// since the runloop will exit before the system has any opportunity to
// capture anything.
bridge_->AcquireCapture();
EXPECT_TRUE(bridge_host_->IsMouseCaptureActive());
// Create the drop data
OSExchangeData data;
const base::string16& text = ASCIIToUTF16("text");
data.SetString(text);
data.provider().SetDragImage(gfx::test::CreateImageSkia(100, 100),
gfx::Vector2d());
SetData(data);
// There's no way to cleanly stop NSDraggingSession inside unit tests, so just
// don't start it at all.
base::mac::ScopedObjCClassSwizzler swizzle(
[NSView class], @selector(beginDraggingSessionWithItems:event:source:),
@selector(cr_beginDraggingSessionWithItems:event:source:));
// Immediately quit drag'n'drop, or we'll hang.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&DragDropClientMac::EndDrag,
base::Unretained(drag_drop_client())));
// It will call ReleaseCapture().
drag_drop_client()->StartDragAndDrop(
target_, data, 0, ui::DragDropTypes::DRAG_EVENT_SOURCE_MOUSE);
// The capture should be released.
EXPECT_FALSE(bridge_host_->IsMouseCaptureActive());
}
// Tests if the drag and drop target rejects the dropped data with the
// incorrect format.
TEST_F(DragDropClientMacTest, InvalidFormatDragDrop) {
OSExchangeData data;
const base::string16& text = ASCIIToUTF16("text");
data.SetString(text);
SetData(data);
target_->set_formats(ui::OSExchangeData::URL);
// Check if the target receives the data from a drop and returns the expected
// operation.
EXPECT_EQ(DragUpdate(nil), NSDragOperationNone);
EXPECT_EQ(Drop(), NSDragOperationNone);
}
// Tests if the drag and drop target can accept data without an OSExchangeData
// object.
TEST_F(DragDropClientMacTest, PasteboardToOSExchangeTest) {
target_->set_formats(ui::OSExchangeData::STRING);
scoped_refptr<ui::UniquePasteboard> pasteboard = new ui::UniquePasteboard;
// The test should reject the data if the pasteboard is empty.
EXPECT_EQ(DragUpdate(pasteboard->get()), NSDragOperationNone);
EXPECT_EQ(Drop(), NSDragOperationNone);
drag_drop_client()->EndDrag();
// Add valid data to the pasteboard and check to see if the target accepts
// it.
[pasteboard->get() setString:@"text" forType:NSPasteboardTypeString];
EXPECT_EQ(DragUpdate(pasteboard->get()), NSDragOperationCopy);
EXPECT_EQ(Drop(), NSDragOperationMove);
}
// View object that will close Widget on drop.
class DragDropCloseView : public DragDropView {
public:
DragDropCloseView() {}
// View:
int OnPerformDrop(const ui::DropTargetEvent& event) override {
GetWidget()->CloseNow();
return ui::DragDropTypes::DRAG_MOVE;
}
private:
DISALLOW_COPY_AND_ASSIGN(DragDropCloseView);
};
// Tests that closing Widget on OnPerformDrop does not crash.
TEST_F(DragDropClientMacTest, CloseWidgetOnDrop) {
OSExchangeData data;
const base::string16& text = ASCIIToUTF16("text");
data.SetString(text);
SetData(data);
target_ = new DragDropCloseView();
widget_->GetContentsView()->AddChildView(target_);
target_->SetBoundsRect(gfx::Rect(0, 0, 100, 100));
target_->set_formats(ui::OSExchangeData::STRING | ui::OSExchangeData::URL);
EXPECT_EQ(DragUpdate(nil), NSDragOperationCopy);
EXPECT_EQ(Drop(), NSDragOperationMove);
// OnPerformDrop() will have deleted the widget.
widget_ = nullptr;
}
} // namespace test
} // namespace views