Reland "iOS spell-checker ObjC #32941" (flutter/engine#34356)

This commit is contained in:
Chris Yang 2022-07-11 16:29:04 -07:00 committed by GitHub
parent 25ed2028ca
commit cd0ced63e5
7 changed files with 558 additions and 0 deletions

View File

@ -1698,6 +1698,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestora
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

View File

@ -79,6 +79,8 @@ source_set("flutter_framework_source") {
"framework/Source/FlutterRestorationPlugin.mm",
"framework/Source/FlutterSemanticsScrollView.h",
"framework/Source/FlutterSemanticsScrollView.mm",
"framework/Source/FlutterSpellCheckPlugin.h",
"framework/Source/FlutterSpellCheckPlugin.mm",
"framework/Source/FlutterTextInputDelegate.h",
"framework/Source/FlutterTextInputPlugin.h",
"framework/Source/FlutterTextInputPlugin.mm",
@ -264,6 +266,7 @@ shared_library("ios_test_flutter") {
"framework/Source/FlutterKeyboardManagerTest.mm",
"framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm",
"framework/Source/FlutterRestorationPluginTest.mm",
"framework/Source/FlutterSpellCheckPluginTest.mm",
"framework/Source/FlutterTextInputPluginTest.mm",
"framework/Source/FlutterUndoManagerPluginTest.mm",
"framework/Source/FlutterViewControllerTest.mm",

View File

@ -24,6 +24,7 @@
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterObservatoryPublisher.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUndoManagerPlugin.h"
@ -116,6 +117,7 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
fml::scoped_nsobject<FlutterPlatformPlugin> _platformPlugin;
fml::scoped_nsobject<FlutterTextInputPlugin> _textInputPlugin;
fml::scoped_nsobject<FlutterUndoManagerPlugin> _undoManagerPlugin;
fml::scoped_nsobject<FlutterSpellCheckPlugin> _spellCheckPlugin;
fml::scoped_nsobject<FlutterRestorationPlugin> _restorationPlugin;
fml::scoped_nsobject<FlutterMethodChannel> _localizationChannel;
fml::scoped_nsobject<FlutterMethodChannel> _navigationChannel;
@ -124,6 +126,7 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
fml::scoped_nsobject<FlutterMethodChannel> _platformViewsChannel;
fml::scoped_nsobject<FlutterMethodChannel> _textInputChannel;
fml::scoped_nsobject<FlutterMethodChannel> _undoManagerChannel;
fml::scoped_nsobject<FlutterMethodChannel> _spellCheckChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _lifecycleChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _systemChannel;
fml::scoped_nsobject<FlutterBasicMessageChannel> _settingsChannel;
@ -469,6 +472,9 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
- (FlutterMethodChannel*)undoManagerChannel {
return _undoManagerChannel.get();
}
- (FlutterMethodChannel*)spellCheckChannel {
return _spellCheckChannel.get();
}
- (FlutterBasicMessageChannel*)lifecycleChannel {
return _lifecycleChannel.get();
}
@ -498,6 +504,7 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
_systemChannel.reset();
_settingsChannel.reset();
_keyEventChannel.reset();
_spellCheckChannel.reset();
}
- (void)startProfiler {
@ -566,6 +573,11 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
binaryMessenger:self.binaryMessenger
codec:[FlutterJSONMethodCodec sharedInstance]]);
_spellCheckChannel.reset([[FlutterMethodChannel alloc]
initWithName:@"flutter/spellcheck"
binaryMessenger:self.binaryMessenger
codec:[FlutterStandardMethodCodec sharedInstance]]);
_lifecycleChannel.reset([[FlutterBasicMessageChannel alloc]
initWithName:@"flutter/lifecycle"
binaryMessenger:self.binaryMessenger
@ -600,6 +612,7 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
_restorationPlugin.reset([[FlutterRestorationPlugin alloc]
initWithChannel:_restorationChannel.get()
restorationEnabled:_restorationEnabled]);
_spellCheckPlugin.reset([[FlutterSpellCheckPlugin alloc] init]);
}
- (void)maybeSetupPlatformViewChannels {
@ -627,6 +640,12 @@ static constexpr int kNumProfilerSamplesPerSec = 5;
setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[undoManagerPlugin handleMethodCall:call result:result];
}];
FlutterSpellCheckPlugin* spellCheckPlugin = _spellCheckPlugin.get();
[_spellCheckChannel.get()
setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[spellCheckPlugin handleMethodCall:call result:result];
}];
}
}

View File

@ -0,0 +1,32 @@
// 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.
#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_
#include "flutter/fml/memory/weak_ptr.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h"
@interface FlutterSpellCheckPlugin : NSObject
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
@end
@interface FlutterSpellCheckResult : NSObject
@property(nonatomic, copy, readonly) NSArray<NSString*>* suggestions;
@property(nonatomic, assign, readonly) NSRange misspelledRange;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)initWithMisspelledRange:(NSRange)range
suggestions:(NSArray<NSString*>*)suggestions NS_DESIGNATED_INITIALIZER;
- (NSDictionary<NSString*, NSObject*>*)toDictionary;
@end
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERSPELLCHECKPLUGIN_H_

View File

@ -0,0 +1,158 @@
// 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/ios/framework/Source/FlutterSpellCheckPlugin.h"
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "flutter/fml/logging.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
// Method Channel name to start spell check.
static NSString* const kInitiateSpellCheck = @"SpellCheck.initiateSpellCheck";
@interface FlutterSpellCheckPlugin ()
@property(nonatomic, retain) UITextChecker* textChecker;
@end
@implementation FlutterSpellCheckPlugin
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if (!_textChecker) {
// UITextChecker is an expensive object to initiate, see:
// https://github.com/flutter/flutter/issues/104454. Lazily initialate the UITextChecker object
// until at first method channel call. We avoid using lazy getter for testing.
_textChecker = [[UITextChecker alloc] init];
}
NSString* method = call.method;
NSArray* args = call.arguments;
if ([method isEqualToString:kInitiateSpellCheck]) {
FML_DCHECK(args.count == 2);
id language = args[0];
id text = args[1];
if (language == [NSNull null] || text == [NSNull null]) {
// Bail if null arguments are passed from dart.
result(nil);
return;
}
NSArray<NSDictionary<NSString*, id>*>* spellCheckResult =
[self findAllSpellCheckSuggestionsForText:text inLanguage:language];
result(spellCheckResult);
}
}
// Get all the misspelled words and suggestions in the entire String.
//
// The result will be formatted as an NSArray.
// Each item of the array is a dictionary representing a misspelled word and suggestions.
// The format looks like:
// {
// startIndex: 0,
// endIndex: 5,
// suggestions: [hello, ...]
// }
//
// Returns nil if the language is invalid.
// Returns an empty array if no spell check suggestions.
- (NSArray<NSDictionary<NSString*, id>*>*)findAllSpellCheckSuggestionsForText:(NSString*)text
inLanguage:(NSString*)language {
if (![UITextChecker.availableLanguages containsObject:language]) {
return nil;
}
NSMutableArray<FlutterSpellCheckResult*>* allSpellSuggestions = [[NSMutableArray alloc] init];
FlutterSpellCheckResult* nextSpellSuggestion;
NSUInteger nextOffset = 0;
do {
nextSpellSuggestion = [self findSpellCheckSuggestionsForText:text
inLanguage:language
startingOffset:nextOffset];
if (nextSpellSuggestion != nil) {
[allSpellSuggestions addObject:nextSpellSuggestion];
nextOffset =
nextSpellSuggestion.misspelledRange.location + nextSpellSuggestion.misspelledRange.length;
}
} while (nextSpellSuggestion != nil && nextOffset < text.length);
NSMutableArray* methodChannelResult = [[[NSMutableArray alloc] init] autorelease];
for (FlutterSpellCheckResult* result in allSpellSuggestions) {
[methodChannelResult addObject:[result toDictionary]];
}
[allSpellSuggestions release];
return methodChannelResult;
}
// Get the misspelled word and suggestions.
//
// Returns nil if no spell check suggestions.
- (FlutterSpellCheckResult*)findSpellCheckSuggestionsForText:(NSString*)text
inLanguage:(NSString*)language
startingOffset:(NSInteger)startingOffset {
FML_DCHECK([UITextChecker.availableLanguages containsObject:language]);
NSRange misspelledRange =
[self.textChecker rangeOfMisspelledWordInString:text
range:NSMakeRange(0, text.length)
startingAt:startingOffset
wrap:NO
language:language];
if (misspelledRange.location == NSNotFound) {
// No misspelled word found
return nil;
}
// If no possible guesses, the API returns an empty array:
// https://developer.apple.com/documentation/uikit/uitextchecker/1621037-guessesforwordrange?language=objc
NSArray<NSString*>* suggestions = [self.textChecker guessesForWordRange:misspelledRange
inString:text
language:language];
FlutterSpellCheckResult* result =
[[[FlutterSpellCheckResult alloc] initWithMisspelledRange:misspelledRange
suggestions:suggestions] autorelease];
return result;
}
- (UITextChecker*)textChecker {
return _textChecker;
}
- (void)dealloc {
[_textChecker release];
[super dealloc];
}
@end
@implementation FlutterSpellCheckResult
- (instancetype)initWithMisspelledRange:(NSRange)range
suggestions:(NSArray<NSString*>*)suggestions {
self = [super init];
if (self) {
_suggestions = [suggestions copy];
_misspelledRange = range;
}
return self;
}
- (NSDictionary<NSString*, NSObject*>*)toDictionary {
NSMutableDictionary* result = [[[NSMutableDictionary alloc] initWithCapacity:3] autorelease];
result[@"startIndex"] = @(_misspelledRange.location);
result[@"endIndex"] = @(_misspelledRange.location + _misspelledRange.length - 1);
result[@"suggestions"] = _suggestions;
return result;
}
- (void)dealloc {
[_suggestions release];
[super dealloc];
}
@end

View File

@ -0,0 +1,341 @@
// 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/ios/framework/Source/FlutterSpellCheckPlugin.h"
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#pragma mark -
// A mock class representing the UITextChecker used in the tests.
//
// Because OCMock doesn't support mocking NSRange as method arguments,
// this is necessary.
@interface MockTextChecker : UITextChecker
// The range of misspelled word based on the startingIndex.
//
// Key is the starting index, value is the range
@property(strong, nonatomic) NSMutableDictionary<NSNumber*, NSValue*>* startingIndexToRange;
// The suggestions of misspelled word based on the starting index of the misspelled word.
//
// Key is a string representing the range of the misspelled word, value is the suggestions.
@property(strong, nonatomic)
NSMutableDictionary<NSString*, NSArray<NSString*>*>* rangeToSuggestions;
// Mock the spell checking results.
//
// When no misspelled word should be detected, pass (NSNotFound, 0) for the `range` parameter, and
// an empty array for `suggestions`.
//
// Call `reset` to remove all the mocks.
- (void)mockResultRange:(NSRange)range
suggestions:(nonnull NSArray<NSString*>*)suggestions
withStartingIndex:(NSInteger)startingIndex;
// Remove all mocks.
- (void)reset;
@end
@implementation MockTextChecker
- (instancetype)init {
self = [super init];
if (self) {
_startingIndexToRange = [[NSMutableDictionary alloc] init];
_rangeToSuggestions = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)mockResultRange:(NSRange)range
suggestions:(NSArray<NSString*>*)suggestions
withStartingIndex:(NSInteger)startingIndex {
NSValue* valueForRange = [NSValue valueWithRange:range];
self.startingIndexToRange[@(startingIndex)] = valueForRange;
NSString* rangeString = NSStringFromRange(valueForRange.rangeValue);
self.rangeToSuggestions[rangeString] = suggestions;
}
- (void)reset {
[self.startingIndexToRange removeAllObjects];
[self.rangeToSuggestions removeAllObjects];
}
#pragma mark UITextChecker Overrides
- (NSRange)rangeOfMisspelledWordInString:(NSString*)stringToCheck
range:(NSRange)range
startingAt:(NSInteger)startingOffset
wrap:(BOOL)wrapFlag
language:(NSString*)language {
return self.startingIndexToRange[@(startingOffset)].rangeValue;
}
- (NSArray<NSString*>*)guessesForWordRange:(NSRange)range
inString:(NSString*)string
language:(NSString*)language {
return self.rangeToSuggestions[NSStringFromRange(range)];
}
@end
@interface FlutterSpellCheckPlugin ()
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
- (UITextChecker*)textChecker;
@end
@interface FlutterSpellCheckPluginTest : XCTestCase
@property(strong, nonatomic) id mockMethodChannel;
@property(strong, nonatomic) FlutterSpellCheckPlugin* plugin;
@property(strong, nonatomic) id mockTextChecker;
@property(strong, nonatomic) id partialMockPlugin;
@end
#pragma mark -
@implementation FlutterSpellCheckPluginTest
- (void)setUp {
[super setUp];
self.mockMethodChannel = OCMClassMock([FlutterMethodChannel class]);
self.plugin = [[FlutterSpellCheckPlugin alloc] init];
__weak FlutterSpellCheckPlugin* weakPlugin = self.plugin;
OCMStub([self.mockMethodChannel invokeMethod:[OCMArg any]
arguments:[OCMArg any]
result:[OCMArg any]])
.andDo(^(NSInvocation* invocation) {
NSString* name;
id args;
FlutterResult result;
[invocation getArgument:&name atIndex:2];
[invocation getArgument:&args atIndex:3];
[invocation getArgument:&result atIndex:4];
FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:name
arguments:args];
[weakPlugin handleMethodCall:methodCall result:result];
});
self.mockTextChecker = [[MockTextChecker alloc] init];
}
- (void)tearDown {
self.plugin = nil;
[super tearDown];
}
#pragma mark - Tests
// Test to make sure the while loop that checks all the misspelled word stops when the a
// `NSNotFound` is found.
- (void)testFindAllSpellCheckSuggestionsForText {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ];
NSArray* suggestions2 = @[ @"suggestion 3", @"suggestion 4" ];
// 0-4 is a misspelled word.
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
startingIndex:0
suggestions:suggestions1];
// 5-9 is a misspelled word.
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(5, 5)
startingIndex:5
suggestions:suggestions2];
// No misspelled word after index 10.
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(NSNotFound, 0)
startingIndex:10
suggestions:@[]];
__block NSArray* capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ @"en", @"ksajlkdf aslkdfl kasdf asdfjk" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertTrue(capturedResult.count == 2);
NSDictionary* suggestionsJSON1 = capturedResult.firstObject;
XCTAssertEqualObjects(suggestionsJSON1[@"startIndex"], @0);
XCTAssertEqualObjects(suggestionsJSON1[@"endIndex"], @4);
XCTAssertEqualObjects(suggestionsJSON1[@"suggestions"], suggestions1);
NSDictionary* suggestionsJSON2 = capturedResult[1];
XCTAssertEqualObjects(suggestionsJSON2[@"startIndex"], @5);
XCTAssertEqualObjects(suggestionsJSON2[@"endIndex"], @9);
XCTAssertEqualObjects(suggestionsJSON2[@"suggestions"], suggestions2);
[self.mockTextChecker reset];
[textCheckerClassMock stopMocking];
}
// Test to make sure while loop that checks all the misspelled word stops when the last word is
// misspelled (aka nextIndex is out of bounds)
- (void)testStopFindingMoreWhenTheLastWordIsMisspelled {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ];
NSArray* suggestions2 = @[ @"suggestion 3", @"suggestion 4" ];
// 0-4 is a misspelled word.
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
startingIndex:0
suggestions:suggestions1];
// 5-9 is a misspelled word.
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(6, 4)
startingIndex:5
suggestions:suggestions2];
__block NSArray* capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ @"en", @"hejjo abcd" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertTrue(capturedResult.count == 2);
NSDictionary* suggestionsJSON1 = capturedResult.firstObject;
XCTAssertEqualObjects(suggestionsJSON1[@"startIndex"], @0);
XCTAssertEqualObjects(suggestionsJSON1[@"endIndex"], @4);
XCTAssertEqualObjects(suggestionsJSON1[@"suggestions"], suggestions1);
NSDictionary* suggestionsJSON2 = capturedResult[1];
XCTAssertEqualObjects(suggestionsJSON2[@"startIndex"], @6);
XCTAssertEqualObjects(suggestionsJSON2[@"endIndex"], @9);
XCTAssertEqualObjects(suggestionsJSON2[@"suggestions"], suggestions2);
[self.mockTextChecker reset];
[textCheckerClassMock stopMocking];
}
- (void)testStopFindingMoreWhenTheWholeStringIsAMisspelledWord {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ];
// 0-4 is a misspelled word.
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
startingIndex:0
suggestions:suggestions1];
__block NSArray* capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ @"en", @"hejjo" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertTrue(capturedResult.count == 1);
NSDictionary* suggestionsJSON1 = capturedResult.firstObject;
XCTAssertEqualObjects(suggestionsJSON1[@"startIndex"], @0);
XCTAssertEqualObjects(suggestionsJSON1[@"endIndex"], @4);
XCTAssertEqualObjects(suggestionsJSON1[@"suggestions"], suggestions1);
[self.mockTextChecker reset];
[textCheckerClassMock stopMocking];
}
- (void)testInitiateSpellCheckWithNoMisspelledWord {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(NSNotFound, 0)
startingIndex:0
suggestions:@[]];
__block id capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ @"en", @"helloo" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertEqualObjects(capturedResult, @[]);
[textCheckerClassMock stopMocking];
}
- (void)testUnsupportedLanguageShouldReturnNil {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
[self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
startingIndex:0
suggestions:@[]];
__block id capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ @"xx", @"helloo" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertNil(capturedResult);
[textCheckerClassMock stopMocking];
}
- (void)testEmptyStringShouldReturnEmptyResults {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
// Use real UITextChecker for this as we want to rely on the actual behavior of UITextChecker
// to ensure that spell checks on an empty result always return empty.
[self.partialMockPlugin stopMocking];
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
__block id capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ @"en", @"" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertEqualObjects(capturedResult, @[]);
[textCheckerClassMock stopMocking];
}
- (void)testNullStringArgumentShouldReturnNilResults {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
__block id capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ @"en", [NSNull null] ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertNil(capturedResult);
[textCheckerClassMock stopMocking];
}
- (void)testNullLanguageArgumentShouldReturnNilResults {
self.partialMockPlugin = OCMPartialMock(self.plugin);
OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
id textCheckerClassMock = OCMClassMock([UITextChecker class]);
[[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
__block id capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ [NSNull null], @"some string" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertNil(capturedResult);
[textCheckerClassMock stopMocking];
}
- (void)testUITextCheckerIsInitializedAfterMethodChannelCall {
XCTAssertNil([self.plugin textChecker]);
__block id capturedResult;
[self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
arguments:@[ [NSNull null], @"some string" ]
result:^(id _Nullable result) {
capturedResult = result;
}];
XCTAssertNotNil([self.plugin textChecker]);
}
- (void)mockUITextCheckerWithExpectedMisspelledWordRange:(NSRange)expectedRange
startingIndex:(NSInteger)startingIndex
suggestions:(NSArray*)suggestions {
[self.mockTextChecker mockResultRange:expectedRange
suggestions:suggestions
withStartingIndex:startingIndex];
}
@end

View File

@ -69,6 +69,7 @@
0D6AB6C922BB05E200EEE540 /* IosUnitTestsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IosUnitTestsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
0D6AB6CF22BB05E200EEE540 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FlutterEngineConfig.xcconfig; sourceTree = "<group>"; };
689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSpellCheckPluginTest.mm; sourceTree = "<group>"; };
68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = "<group>"; };
3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterUndoManagerPluginTest.mm; sourceTree = "<group>"; };
F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = "<group>"; };
@ -98,6 +99,7 @@
0AC232E924BA71D300A85907 /* Source */ = {
isa = PBXGroup;
children = (
689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */,
68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */,
3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */,
F7A3FDE026B9E0A300EADD61 /* FlutterAppDelegateTest.mm */,