mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Reland "iOS spell-checker ObjC #32941" (flutter/engine#34356)
This commit is contained in:
parent
25ed2028ca
commit
cd0ced63e5
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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_
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 */,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user