diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 12cc313c5b583..cfc0e0fd4aeea 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1035,6 +1035,7 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterTextI FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterView.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/fixtures/flutter_desktop_test.dart FILE: ../../../flutter/shell/platform/darwin/macos/framework/module.modulemap diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index bacb6af24a1b2..0eb36f594a56a 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -101,7 +101,10 @@ test_fixtures("flutter_desktop_darwin_fixtures") { executable("flutter_desktop_darwin_unittests") { testonly = true - sources = [ "framework/Source/FlutterEngineUnittests.mm" ] + sources = [ + "framework/Source/FlutterEngineUnittests.mm", + "framework/Source/FlutterViewControllerTest.mm", + ] cflags_objcc = [ "-fobjc-arc" ] @@ -114,6 +117,7 @@ executable("flutter_desktop_darwin_unittests") { "//flutter/testing:dart", "//flutter/testing:skia", "//flutter/testing:testing_lib", + "//third_party/ocmock:ocmock", ] } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index b1a6055e28f70..e30dbc7d9ae3f 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -173,6 +173,11 @@ - (NSDictionary*)getClipboardData:(NSString*)format; */ - (void)setClipboardData:(NSDictionary*)data; +/** + * Returns true iff the clipboard contains nonempty string data. + */ +- (BOOL)clipboardHasStrings; + @end #pragma mark - FlutterViewController implementation. @@ -505,6 +510,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([call.method isEqualToString:@"Clipboard.setData"]) { [self setClipboardData:call.arguments]; result(nil); + } else if ([call.method isEqualToString:@"Clipboard.hasStrings"]) { + result(@{@"value" : @([self clipboardHasStrings])}); } else { result(FlutterMethodNotImplemented); } @@ -517,7 +524,7 @@ - (void)playSystemSound:(NSString*)soundType { } - (NSDictionary*)getClipboardData:(NSString*)format { - NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + NSPasteboard* pasteboard = self.pasteboard; if ([format isEqualToString:@(kTextPlainFormat)]) { NSString* stringInPasteboard = [pasteboard stringForType:NSPasteboardTypeString]; return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard}; @@ -526,14 +533,22 @@ - (NSDictionary*)getClipboardData:(NSString*)format { } - (void)setClipboardData:(NSDictionary*)data { - NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + NSPasteboard* pasteboard = self.pasteboard; NSString* text = data[@"text"]; + [pasteboard clearContents]; if (text && ![text isEqual:[NSNull null]]) { - [pasteboard clearContents]; [pasteboard setString:text forType:NSPasteboardTypeString]; } } +- (BOOL)clipboardHasStrings { + return [self.pasteboard stringForType:NSPasteboardTypeString].length > 0; +} + +- (NSPasteboard*)pasteboard { + return [NSPasteboard generalPasteboard]; +} + #pragma mark - FlutterViewReshapeListener /** diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm new file mode 100644 index 0000000000000..505a573c045a8 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -0,0 +1,75 @@ +// Copyright 2013 The Flutter 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 "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" + +#include "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" +#include "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" +#include "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" +#include "flutter/testing/testing.h" +#import "third_party/ocmock/Source/OCMock/OCMock.h" + +namespace flutter::testing { + +// Returns a mock FlutterViewController that is able to work in environments +// without a real pasteboard. +id mockViewController(NSString* pasteboardString) { + NSString* fixtures = @(testing::GetFixturesPath()); + FlutterDartProject* project = [[FlutterDartProject alloc] + initWithAssetsPath:fixtures + ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + + // Mock pasteboard so that this test will work in environments without a + // real pasteboard. + id pasteboardMock = OCMClassMock([NSPasteboard class]); + OCMExpect([pasteboardMock stringForType:[OCMArg any]]).andDo(^(NSInvocation* invocation) { + NSString* returnValue = pasteboardString.length > 0 ? pasteboardString : nil; + [invocation setReturnValue:&returnValue]; + }); + id viewControllerMock = OCMPartialMock(viewController); + OCMStub([viewControllerMock pasteboard]).andReturn(pasteboardMock); + return viewControllerMock; +} + +TEST(FlutterViewControllerTest, HasStringsWhenPasteboardEmpty) { + // Mock FlutterViewController so that it behaves like the pasteboard is empty. + id viewControllerMock = mockViewController(nil); + + // Call hasStrings and expect it to be false. + __block bool calledAfterClear = false; + __block bool valueAfterClear; + FlutterResult resultAfterClear = ^(id result) { + calledAfterClear = true; + NSNumber* valueNumber = [result valueForKey:@"value"]; + valueAfterClear = [valueNumber boolValue]; + }; + FlutterMethodCall* methodCallAfterClear = + [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil]; + [viewControllerMock handleMethodCall:methodCallAfterClear result:resultAfterClear]; + ASSERT_TRUE(calledAfterClear); + ASSERT_FALSE(valueAfterClear); +} + +TEST(FlutterViewControllerTest, HasStringsWhenPasteboardFull) { + // Mock FlutterViewController so that it behaves like the pasteboard has a + // valid string. + id viewControllerMock = mockViewController(@"some string"); + + // Call hasStrings and expect it to be true. + __block bool called = false; + __block bool value; + FlutterResult result = ^(id result) { + called = true; + NSNumber* valueNumber = [result valueForKey:@"value"]; + value = [valueNumber boolValue]; + }; + FlutterMethodCall* methodCall = + [FlutterMethodCall methodCallWithMethodName:@"Clipboard.hasStrings" arguments:nil]; + [viewControllerMock handleMethodCall:methodCall result:result]; + ASSERT_TRUE(called); + ASSERT_TRUE(value); +} + +} // flutter::testing diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index 5edd87c0f71c1..a2202fe9498a9 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -11,6 +11,11 @@ // The FlutterView for this view controller. @property(nonatomic, readonly, nullable) FlutterView* flutterView; +/** + * This just returns the NSPasteboard so that it can be mocked in the tests. + */ +@property(nonatomic, readonly, nonnull) NSPasteboard* pasteboard; + /** * Adds a responder for keyboard events. Key up and key down events are forwarded to all added * responders.