Flutter iOS Embedder
FlutterSpellCheckPluginTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #import <OCMock/OCMock.h>
8 #import <XCTest/XCTest.h>
9 
10 #pragma mark -
11 
12 // A mock class representing the UITextChecker used in the tests.
13 //
14 // Because OCMock doesn't support mocking NSRange as method arguments,
15 // this is necessary.
16 @interface MockTextChecker : UITextChecker
17 
18 // The range of misspelled word based on the startingIndex.
19 //
20 // Key is the starting index, value is the range
21 @property(strong, nonatomic) NSMutableDictionary<NSNumber*, NSValue*>* startingIndexToRange;
22 
23 // The suggestions of misspelled word based on the starting index of the misspelled word.
24 //
25 // Key is a string representing the range of the misspelled word, value is the suggestions.
26 @property(strong, nonatomic)
27  NSMutableDictionary<NSString*, NSArray<NSString*>*>* rangeToSuggestions;
28 
29 // Mock the spell checking results.
30 //
31 // When no misspelled word should be detected, pass (NSNotFound, 0) for the `range` parameter, and
32 // an empty array for `suggestions`.
33 //
34 // Call `reset` to remove all the mocks.
35 - (void)mockResultRange:(NSRange)range
36  suggestions:(nonnull NSArray<NSString*>*)suggestions
37  withStartingIndex:(NSInteger)startingIndex;
38 
39 // Remove all mocks.
40 - (void)reset;
41 
42 @end
43 
44 @implementation MockTextChecker
45 
46 - (instancetype)init {
47  self = [super init];
48  if (self) {
49  _startingIndexToRange = [[NSMutableDictionary alloc] init];
50  _rangeToSuggestions = [[NSMutableDictionary alloc] init];
51  }
52  return self;
53 }
54 
55 - (void)mockResultRange:(NSRange)range
56  suggestions:(NSArray<NSString*>*)suggestions
57  withStartingIndex:(NSInteger)startingIndex {
58  NSValue* valueForRange = [NSValue valueWithRange:range];
59  self.startingIndexToRange[@(startingIndex)] = valueForRange;
60  NSString* rangeString = NSStringFromRange(valueForRange.rangeValue);
61  self.rangeToSuggestions[rangeString] = suggestions;
62 }
63 
64 - (void)reset {
65  [self.startingIndexToRange removeAllObjects];
66  [self.rangeToSuggestions removeAllObjects];
67 }
68 
69 #pragma mark UITextChecker Overrides
70 
71 - (NSRange)rangeOfMisspelledWordInString:(NSString*)stringToCheck
72  range:(NSRange)range
73  startingAt:(NSInteger)startingOffset
74  wrap:(BOOL)wrapFlag
75  language:(NSString*)language {
76  return self.startingIndexToRange[@(startingOffset)].rangeValue;
77 }
78 
79 - (NSArray<NSString*>*)guessesForWordRange:(NSRange)range
80  inString:(NSString*)string
81  language:(NSString*)language {
82  return self.rangeToSuggestions[NSStringFromRange(range)];
83 }
84 
85 @end
86 
87 @interface FlutterSpellCheckPlugin ()
88 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
89 - (UITextChecker*)textChecker;
90 @end
91 
92 @interface FlutterSpellCheckPluginTest : XCTestCase
93 
94 @property(strong, nonatomic) id mockMethodChannel;
95 @property(strong, nonatomic) FlutterSpellCheckPlugin* plugin;
96 @property(strong, nonatomic) id mockTextChecker;
97 @property(strong, nonatomic) id partialMockPlugin;
98 
99 @end
100 
101 #pragma mark -
102 
103 @implementation FlutterSpellCheckPluginTest
104 
105 - (void)setUp {
106  [super setUp];
107  self.mockMethodChannel = OCMClassMock([FlutterMethodChannel class]);
108  self.plugin = [[FlutterSpellCheckPlugin alloc] init];
109  __weak FlutterSpellCheckPlugin* weakPlugin = self.plugin;
110  OCMStub([self.mockMethodChannel invokeMethod:[OCMArg any]
111  arguments:[OCMArg any]
112  result:[OCMArg any]])
113  .andDo(^(NSInvocation* invocation) {
114  NSString* name;
115  id args;
116  FlutterResult result;
117  [invocation getArgument:&name atIndex:2];
118  [invocation getArgument:&args atIndex:3];
119  [invocation getArgument:&result atIndex:4];
121  arguments:args];
122  [weakPlugin handleMethodCall:methodCall result:result];
123  });
124  self.mockTextChecker = [[MockTextChecker alloc] init];
125 }
126 
127 - (void)tearDown {
128  self.plugin = nil;
129  [super tearDown];
130 }
131 
132 #pragma mark - Tests
133 
134 // Test to make sure the while loop that checks all the misspelled word stops when the a
135 // `NSNotFound` is found.
136 - (void)testFindAllSpellCheckSuggestionsForText {
137  self.partialMockPlugin = OCMPartialMock(self.plugin);
138  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
139  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
140  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
141  NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ];
142  NSArray* suggestions2 = @[ @"suggestion 3", @"suggestion 4" ];
143  // 0-4 is a misspelled word.
144  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
145  startingIndex:0
146  suggestions:suggestions1];
147  // 5-9 is a misspelled word.
148  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(5, 5)
149  startingIndex:5
150  suggestions:suggestions2];
151  // No misspelled word after index 10.
152  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(NSNotFound, 0)
153  startingIndex:10
154  suggestions:@[]];
155  __block NSArray* capturedResult;
156  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
157  arguments:@[ @"en", @"ksajlkdf aslkdfl kasdf asdfjk" ]
158  result:^(id _Nullable result) {
159  capturedResult = result;
160  }];
161  XCTAssertTrue(capturedResult.count == 2);
162  NSDictionary* suggestionsJSON1 = capturedResult.firstObject;
163  XCTAssertEqualObjects(suggestionsJSON1[@"startIndex"], @0);
164  XCTAssertEqualObjects(suggestionsJSON1[@"endIndex"], @5);
165  XCTAssertEqualObjects(suggestionsJSON1[@"suggestions"], suggestions1);
166  NSDictionary* suggestionsJSON2 = capturedResult[1];
167  XCTAssertEqualObjects(suggestionsJSON2[@"startIndex"], @5);
168  XCTAssertEqualObjects(suggestionsJSON2[@"endIndex"], @10);
169  XCTAssertEqualObjects(suggestionsJSON2[@"suggestions"], suggestions2);
170  [self.mockTextChecker reset];
171  [textCheckerClassMock stopMocking];
172 }
173 
174 // Test to make sure while loop that checks all the misspelled word stops when the last word is
175 // misspelled (aka nextIndex is out of bounds)
176 - (void)testStopFindingMoreWhenTheLastWordIsMisspelled {
177  self.partialMockPlugin = OCMPartialMock(self.plugin);
178  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
179  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
180  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
181  NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ];
182  NSArray* suggestions2 = @[ @"suggestion 3", @"suggestion 4" ];
183  // 0-4 is a misspelled word.
184  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
185  startingIndex:0
186  suggestions:suggestions1];
187  // 5-9 is a misspelled word.
188  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(6, 4)
189  startingIndex:5
190  suggestions:suggestions2];
191 
192  __block NSArray* capturedResult;
193  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
194  arguments:@[ @"en", @"hejjo abcd" ]
195  result:^(id _Nullable result) {
196  capturedResult = result;
197  }];
198  XCTAssertTrue(capturedResult.count == 2);
199  NSDictionary* suggestionsJSON1 = capturedResult.firstObject;
200  XCTAssertEqualObjects(suggestionsJSON1[@"startIndex"], @0);
201  XCTAssertEqualObjects(suggestionsJSON1[@"endIndex"], @5);
202  XCTAssertEqualObjects(suggestionsJSON1[@"suggestions"], suggestions1);
203  NSDictionary* suggestionsJSON2 = capturedResult[1];
204  XCTAssertEqualObjects(suggestionsJSON2[@"startIndex"], @6);
205  XCTAssertEqualObjects(suggestionsJSON2[@"endIndex"], @10);
206  XCTAssertEqualObjects(suggestionsJSON2[@"suggestions"], suggestions2);
207  [self.mockTextChecker reset];
208  [textCheckerClassMock stopMocking];
209 }
210 
211 - (void)testStopFindingMoreWhenTheWholeStringIsAMisspelledWord {
212  self.partialMockPlugin = OCMPartialMock(self.plugin);
213  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
214  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
215  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
216  NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ];
217  // 0-4 is a misspelled word.
218  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
219  startingIndex:0
220  suggestions:suggestions1];
221 
222  __block NSArray* capturedResult;
223  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
224  arguments:@[ @"en", @"hejjo" ]
225  result:^(id _Nullable result) {
226  capturedResult = result;
227  }];
228  XCTAssertTrue(capturedResult.count == 1);
229  NSDictionary* suggestionsJSON1 = capturedResult.firstObject;
230  XCTAssertEqualObjects(suggestionsJSON1[@"startIndex"], @0);
231  XCTAssertEqualObjects(suggestionsJSON1[@"endIndex"], @5);
232  XCTAssertEqualObjects(suggestionsJSON1[@"suggestions"], suggestions1);
233  [self.mockTextChecker reset];
234  [textCheckerClassMock stopMocking];
235 }
236 
237 - (void)testInitiateSpellCheckWithNoMisspelledWord {
238  self.partialMockPlugin = OCMPartialMock(self.plugin);
239  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
240  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
241  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
242  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(NSNotFound, 0)
243  startingIndex:0
244  suggestions:@[]];
245  __block id capturedResult;
246  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
247  arguments:@[ @"en", @"helloo" ]
248  result:^(id _Nullable result) {
249  capturedResult = result;
250  }];
251  XCTAssertEqualObjects(capturedResult, @[]);
252  [textCheckerClassMock stopMocking];
253 }
254 
255 - (void)testUnsupportedLanguageShouldReturnNil {
256  self.partialMockPlugin = OCMPartialMock(self.plugin);
257  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
258  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
259  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
260  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
261  startingIndex:0
262  suggestions:@[]];
263  __block id capturedResult;
264  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
265  arguments:@[ @"xx", @"helloo" ]
266  result:^(id _Nullable result) {
267  capturedResult = result;
268  }];
269  XCTAssertNil(capturedResult);
270  [textCheckerClassMock stopMocking];
271 }
272 
273 - (void)testSupportSubLanguage {
274  self.partialMockPlugin = OCMPartialMock(self.plugin);
275  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
276  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
277  [[[textCheckerClassMock stub] andReturn:@[ @"en_US" ]] availableLanguages];
278  NSArray* suggestions1 = @[ @"suggestion 1", @"suggestion 2" ];
279 
280  [self mockUITextCheckerWithExpectedMisspelledWordRange:NSMakeRange(0, 5)
281  startingIndex:0
282  suggestions:suggestions1];
283  __block NSArray* capturedResult;
284  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
285  arguments:@[ @"en-us", @"hejjo" ]
286  result:^(id _Nullable result) {
287  capturedResult = result;
288  }];
289  NSDictionary* suggestionsJSON1 = capturedResult.firstObject;
290  XCTAssertEqualObjects(suggestionsJSON1[@"startIndex"], @0);
291  XCTAssertEqualObjects(suggestionsJSON1[@"endIndex"], @5);
292  XCTAssertEqualObjects(suggestionsJSON1[@"suggestions"], suggestions1);
293  [textCheckerClassMock stopMocking];
294 }
295 
296 - (void)testEmptyStringShouldReturnEmptyResults {
297  self.partialMockPlugin = OCMPartialMock(self.plugin);
298  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
299  // Use real UITextChecker for this as we want to rely on the actual behavior of UITextChecker
300  // to ensure that spell checks on an empty result always return empty.
301  [self.partialMockPlugin stopMocking];
302 
303  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
304  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
305  __block id capturedResult;
306  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
307  arguments:@[ @"en", @"" ]
308  result:^(id _Nullable result) {
309  capturedResult = result;
310  }];
311  XCTAssertEqualObjects(capturedResult, @[]);
312  [textCheckerClassMock stopMocking];
313 }
314 
315 - (void)testNullStringArgumentShouldReturnNilResults {
316  self.partialMockPlugin = OCMPartialMock(self.plugin);
317  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
318  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
319  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
320  __block id capturedResult;
321  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
322  arguments:@[ @"en", [NSNull null] ]
323  result:^(id _Nullable result) {
324  capturedResult = result;
325  }];
326  XCTAssertNil(capturedResult);
327  [textCheckerClassMock stopMocking];
328 }
329 
330 - (void)testNullLanguageArgumentShouldReturnNilResults {
331  self.partialMockPlugin = OCMPartialMock(self.plugin);
332  OCMStub([self.partialMockPlugin textChecker]).andReturn(self.mockTextChecker);
333  id textCheckerClassMock = OCMClassMock([UITextChecker class]);
334  [[[textCheckerClassMock stub] andReturn:@[ @"en" ]] availableLanguages];
335  __block id capturedResult;
336  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
337  arguments:@[ [NSNull null], @"some string" ]
338  result:^(id _Nullable result) {
339  capturedResult = result;
340  }];
341  XCTAssertNil(capturedResult);
342  [textCheckerClassMock stopMocking];
343 }
344 
345 - (void)testUITextCheckerIsInitializedAfterMethodChannelCall {
346  XCTAssertNil([self.plugin textChecker]);
347  __block id capturedResult;
348  [self.mockMethodChannel invokeMethod:@"SpellCheck.initiateSpellCheck"
349  arguments:@[ [NSNull null], @"some string" ]
350  result:^(id _Nullable result) {
351  capturedResult = result;
352  }];
353  XCTAssertNotNil([self.plugin textChecker]);
354 }
355 
356 - (void)mockUITextCheckerWithExpectedMisspelledWordRange:(NSRange)expectedRange
357  startingIndex:(NSInteger)startingIndex
358  suggestions:(NSArray*)suggestions {
359  [self.mockTextChecker mockResultRange:expectedRange
360  suggestions:suggestions
361  withStartingIndex:startingIndex];
362 }
363 
364 @end
FlutterSpellCheckPlugin
Definition: FlutterSpellCheckPlugin.h:13
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
-[MockTextChecker reset]
void reset()
Definition: FlutterSpellCheckPluginTest.mm:64
FlutterMethodChannel
Definition: FlutterChannels.h:220
FlutterSpellCheckPluginTest::mockMethodChannel
id mockMethodChannel
Definition: FlutterSpellCheckPluginTest.mm:94
MockTextChecker::startingIndexToRange
NSMutableDictionary< NSNumber *, NSValue * > * startingIndexToRange
Definition: FlutterSpellCheckPluginTest.mm:21
FlutterSpellCheckPluginTest::plugin
FlutterSpellCheckPlugin * plugin
Definition: FlutterSpellCheckPluginTest.mm:95
MockTextChecker::rangeToSuggestions
NSMutableDictionary< NSString *, NSArray< NSString * > * > * rangeToSuggestions
Definition: FlutterSpellCheckPluginTest.mm:27
FlutterMethodCall
Definition: FlutterCodecs.h:220
FlutterSpellCheckPluginTest
Definition: FlutterSpellCheckPluginTest.mm:92
FlutterSpellCheckPlugin.h
MockTextChecker
Definition: FlutterSpellCheckPluginTest.mm:16
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
FlutterSpellCheckPluginTest::partialMockPlugin
id partialMockPlugin
Definition: FlutterSpellCheckPluginTest.mm:97
FlutterSpellCheckPluginTest::mockTextChecker
id mockTextChecker
Definition: FlutterSpellCheckPluginTest.mm:96