| 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
5 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h"
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];
120 FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:name
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
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
FlutterSpellCheckPlugin.h
FlutterMethodCall
Definition: FlutterCodecs.h:221
+[FlutterMethodCall methodCallWithMethodName:arguments:]
instancetype methodCallWithMethodName:arguments:(NSString *method,[arguments] id _Nullable arguments)
FlutterMethodChannel
Definition: FlutterChannels.h:221
FlutterSpellCheckPlugin
Definition: FlutterSpellCheckPlugin.h:12
FlutterSpellCheckPluginTest
Definition: FlutterSpellCheckPluginTest.mm:93
FlutterSpellCheckPluginTest::mockMethodChannel
id mockMethodChannel
Definition: FlutterSpellCheckPluginTest.mm:94
FlutterSpellCheckPluginTest::plugin
FlutterSpellCheckPlugin * plugin
Definition: FlutterSpellCheckPluginTest.mm:95
FlutterSpellCheckPluginTest::partialMockPlugin
id partialMockPlugin
Definition: FlutterSpellCheckPluginTest.mm:97
FlutterSpellCheckPluginTest::mockTextChecker
id mockTextChecker
Definition: FlutterSpellCheckPluginTest.mm:96
MockTextChecker
Definition: FlutterSpellCheckPluginTest.mm:17
MockTextChecker::startingIndexToRange
NSMutableDictionary< NSNumber *, NSValue * > * startingIndexToRange
Definition: FlutterSpellCheckPluginTest.mm:21
MockTextChecker::rangeToSuggestions
NSMutableDictionary< NSString *, NSArray< NSString * > * > * rangeToSuggestions
Definition: FlutterSpellCheckPluginTest.mm:27
-[MockTextChecker reset]
void reset()
Definition: FlutterSpellCheckPluginTest.mm:64