| Flutter iOS Embedder |

FlutterKeyboardManagerTest.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 <Foundation/Foundation.h>
6 #import <OCMock/OCMock.h>
7 #import <UIKit/UIKit.h>
8 #import <XCTest/XCTest.h>
9 #include <_types/_uint32_t.h>
10
11 #include "flutter/fml/platform/darwin/message_loop_darwin.h"
12 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
13 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
14 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
15 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.h"
16 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
17
18 FLUTTER_ASSERT_ARC;
19
20 namespace flutter {
21 class PointerDataPacket {};
22 } // namespace flutter
23
24 using namespace flutter::testing;
25
26 /// Sometimes we have to use a custom mock to avoid retain cycles in ocmock.
27 @interface FlutterEnginePartialMock : FlutterEngine
28 @property(nonatomic, strong) FlutterBasicMessageChannel* lifecycleChannel;
29 @property(nonatomic, weak) FlutterViewController* viewController;
30 @property(nonatomic, assign) BOOL didCallNotifyLowMemory;
31 @end
32
33 @interface FlutterEngine ()
34 - (BOOL)createShell:(NSString*)entrypoint
35 libraryURI:(NSString*)libraryURI
36 initialRoute:(NSString*)initialRoute;
37 - (void)dispatchPointerDataPacket:(std::unique_ptr<flutter::PointerDataPacket>)packet;
38 @end
39
40 @interface FlutterEngine (TestLowMemory)
41 - (void)notifyLowMemory;
42 @end
43
44 extern NSNotificationName const FlutterViewControllerWillDealloc;
45
46 /// A simple mock class for FlutterEngine.
47 ///
48 /// OCMockClass can't be used for FlutterEngine sometimes because OCMock retains arguments to
49 /// invocations and since the init for FlutterViewController calls a method on the
50 /// FlutterEngine it creates a retain cycle that stops us from testing behaviors related to
51 /// deleting FlutterViewControllers.
52 @interface MockEngine : NSObject
53 @end
54
55 @interface FlutterKeyboardManagerUnittestsObjC : NSObject
56 - (bool)nextResponderShouldThrowOnPressesEnded;
57 - (bool)singlePrimaryResponder;
58 - (bool)doublePrimaryResponder;
59 - (bool)singleSecondaryResponder;
60 - (bool)emptyNextResponder;
61 @end
62
63 namespace {
64
65 typedef void (^KeyCallbackSetter)(FlutterUIPressProxy* press, FlutterAsyncKeyCallback callback)
66 API_AVAILABLE(ios(13.4));
67 typedef BOOL (^BoolGetter)();
68
69 } // namespace
70
71 @interface FlutterKeyboardManagerTest : XCTestCase
72 @property(nonatomic, strong) id mockEngine;
73 - (FlutterViewController*)mockOwnerWithPressesBeginOnlyNext API_AVAILABLE(ios(13.4));
74 @end
75
76 @implementation FlutterKeyboardManagerTest
77
78 - (void)setUp {
79 // All of these tests were designed to run on iOS 13.4 or later.
80 if (@available(iOS 13.4, *)) {
81 } else {
82 XCTSkip(@"Required API not present for test.");
83 }
84
85 [super setUp];
86 self.mockEngine = OCMClassMock([FlutterEngine class]);
87 }
88
89 - (void)tearDown {
90 // We stop mocking here to avoid retain cycles that stop
91 // FlutterViewControllers from deallocing.
92 [self.mockEngine stopMocking];
93 self.mockEngine = nil;
94 [super tearDown];
95 }
96
97 - (id)checkKeyDownEvent:(UIKeyboardHIDUsage)keyCode API_AVAILABLE(ios(13.4)) {
98 return [OCMArg checkWithBlock:^BOOL(id value) {
99 if (![value isKindOfClass:[FlutterUIPressProxy class]]) {
100 return NO;
101 }
102 FlutterUIPressProxy* press = value;
103 return press.key.keyCode == keyCode;
104 }];
105 }
106
107 - (id<FlutterKeyPrimaryResponder>)mockPrimaryResponder:(KeyCallbackSetter)callbackSetter
108 API_AVAILABLE(ios(13.4)) {
109 id<FlutterKeyPrimaryResponder> mock =
110 OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder));
111 OCMStub([mock handlePress:[OCMArg any] callback:[OCMArg any]])
112 .andDo((^(NSInvocation* invocation) {
113 FlutterUIPressProxy* press;
114 FlutterAsyncKeyCallback callback;
115 [invocation getArgument:&press atIndex:2];
116 [invocation getArgument:&callback atIndex:3];
117 CFRunLoopPerformBlock(CFRunLoopGetCurrent(),
118 fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, ^() {
119 callbackSetter(press, callback);
120 });
121 }));
122 return mock;
123 }
124
125 - (id<FlutterKeySecondaryResponder>)mockSecondaryResponder:(BoolGetter)resultGetter
126 API_AVAILABLE(ios(13.4)) {
127 id<FlutterKeySecondaryResponder> mock =
128 OCMStrictProtocolMock(@protocol(FlutterKeySecondaryResponder));
129 OCMStub([mock handlePress:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
130 BOOL result = resultGetter();
131 [invocation setReturnValue:&result];
132 }));
133 return mock;
134 }
135
136 - (FlutterViewController*)mockOwnerWithPressesBeginOnlyNext API_AVAILABLE(ios(13.4)) {
137 // The nextResponder is a strict mock and hasn't stubbed pressesEnded.
138 // An error will be thrown on pressesEnded.
139 UIResponder* nextResponder = OCMStrictClassMock([UIResponder class]);
140 OCMStub([nextResponder pressesBegan:[OCMArg any] withEvent:[OCMArg any]]).andDo(nil);
141
142 FlutterViewController* viewController =
143 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
144 FlutterViewController* owner = OCMPartialMock(viewController);
145 OCMStub([owner nextResponder]).andReturn(nextResponder);
146 return owner;
147 }
148
149 // Verify that the nextResponder returned from mockOwnerWithPressesBeginOnlyNext()
150 // throws exception when pressesEnded is called.
151 - (bool)testNextResponderShouldThrowOnPressesEnded API_AVAILABLE(ios(13.4)) {
152 FlutterViewController* owner = [self mockOwnerWithPressesBeginOnlyNext];
153 @try {
154 [owner.nextResponder pressesEnded:[NSSet init] withEvent:[[UIPressesEvent alloc] init]];
155 return false;
156 } @catch (...) {
157 return true;
158 }
159 }
160
161 - (void)testSinglePrimaryResponder API_AVAILABLE(ios(13.4)) {
162 FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
163 __block BOOL primaryResponse = FALSE;
164 __block int callbackCount = 0;
165 [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
166 FlutterAsyncKeyCallback callback) {
167 callbackCount++;
168 callback(primaryResponse);
169 }]];
170 constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
171 // Case: The responder reports TRUE
172 __block bool completeHandled = true;
173 primaryResponse = TRUE;
174 [manager handlePress:keyDownEvent(keyId)
175 nextAction:^() {
176 completeHandled = false;
177 }];
178 XCTAssertEqual(callbackCount, 1);
179 XCTAssertTrue(completeHandled);
180 completeHandled = true;
181 callbackCount = 0;
182
183 // Case: The responder reports FALSE
184 primaryResponse = FALSE;
185 [manager handlePress:keyUpEvent(keyId)
186 nextAction:^() {
187 completeHandled = false;
188 }];
189 XCTAssertEqual(callbackCount, 1);
190 XCTAssertFalse(completeHandled);
191 }
192
193 - (void)testDoublePrimaryResponder API_AVAILABLE(ios(13.4)) {
194 FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
195
196 __block BOOL callback1Response = FALSE;
197 __block int callback1Count = 0;
198 [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
199 FlutterAsyncKeyCallback callback) {
200 callback1Count++;
201 callback(callback1Response);
202 }]];
203
204 __block BOOL callback2Response = FALSE;
205 __block int callback2Count = 0;
206 [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
207 FlutterAsyncKeyCallback callback) {
208 callback2Count++;
209 callback(callback2Response);
210 }]];
211
212 // Case: Both responders report TRUE.
213 __block bool somethingWasHandled = true;
214 constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
215 callback1Response = TRUE;
216 callback2Response = TRUE;
217 [manager handlePress:keyUpEvent(keyId)
218 nextAction:^() {
219 somethingWasHandled = false;
220 }];
221 XCTAssertEqual(callback1Count, 1);
222 XCTAssertEqual(callback2Count, 1);
223 XCTAssertTrue(somethingWasHandled);
224
225 somethingWasHandled = true;
226 callback1Count = 0;
227 callback2Count = 0;
228
229 // Case: One responder reports TRUE.
230 callback1Response = TRUE;
231 callback2Response = FALSE;
232 [manager handlePress:keyUpEvent(keyId)
233 nextAction:^() {
234 somethingWasHandled = false;
235 }];
236 XCTAssertEqual(callback1Count, 1);
237 XCTAssertEqual(callback2Count, 1);
238 XCTAssertTrue(somethingWasHandled);
239
240 somethingWasHandled = true;
241 callback1Count = 0;
242 callback2Count = 0;
243
244 // Case: Both responders report FALSE.
245 callback1Response = FALSE;
246 callback2Response = FALSE;
247 [manager handlePress:keyDownEvent(keyId)
248 nextAction:^() {
249 somethingWasHandled = false;
250 }];
251 XCTAssertEqual(callback1Count, 1);
252 XCTAssertEqual(callback2Count, 1);
253 XCTAssertFalse(somethingWasHandled);
254 }
255
256 - (void)testSingleSecondaryResponder API_AVAILABLE(ios(13.4)) {
257 FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
258
259 __block BOOL primaryResponse = FALSE;
260 __block int callbackCount = 0;
261 [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
262 FlutterAsyncKeyCallback callback) {
263 callbackCount++;
264 callback(primaryResponse);
265 }]];
266
267 __block BOOL secondaryResponse;
268 [manager addSecondaryResponder:[self mockSecondaryResponder:^() {
269 return secondaryResponse;
270 }]];
271
272 // Case: Primary responder responds TRUE. The event shouldn't be handled by
273 // the secondary responder.
274 constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
275 secondaryResponse = FALSE;
276 primaryResponse = TRUE;
277 __block bool completeHandled = true;
278 [manager handlePress:keyUpEvent(keyId)
279 nextAction:^() {
280 completeHandled = false;
281 }];
282 XCTAssertEqual(callbackCount, 1);
283 XCTAssertTrue(completeHandled);
284 completeHandled = true;
285 callbackCount = 0;
286
287 // Case: Primary responder responds FALSE. The secondary responder returns
288 // TRUE.
289 secondaryResponse = TRUE;
290 primaryResponse = FALSE;
291 [manager handlePress:keyUpEvent(keyId)
292 nextAction:^() {
293 completeHandled = false;
294 }];
295 XCTAssertEqual(callbackCount, 1);
296 XCTAssertTrue(completeHandled);
297 completeHandled = true;
298 callbackCount = 0;
299
300 // Case: Primary responder responds FALSE. The secondary responder returns FALSE.
301 secondaryResponse = FALSE;
302 primaryResponse = FALSE;
303 [manager handlePress:keyDownEvent(keyId)
304 nextAction:^() {
305 completeHandled = false;
306 }];
307 XCTAssertEqual(callbackCount, 1);
308 XCTAssertFalse(completeHandled);
309 }
310
311 - (void)testEventsProcessedSequentially API_AVAILABLE(ios(13.4)) {
312 constexpr UIKeyboardHIDUsage keyId1 = (UIKeyboardHIDUsage)0x50;
313 constexpr UIKeyboardHIDUsage keyId2 = (UIKeyboardHIDUsage)0x51;
314 FlutterUIPressProxy* event1 = keyDownEvent(keyId1);
315 FlutterUIPressProxy* event2 = keyDownEvent(keyId2);
316 __block FlutterAsyncKeyCallback key1Callback;
317 __block FlutterAsyncKeyCallback key2Callback;
318 __block bool key1Handled = true;
319 __block bool key2Handled = true;
320
321 FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
322 [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
323 FlutterAsyncKeyCallback callback) {
324 if (press == event1) {
325 key1Callback = callback;
326 } else if (press == event2) {
327 key2Callback = callback;
328 }
329 }]];
330
331 // Add both presses into the main CFRunLoop queue
332 CFRunLoopTimerRef timer0 = CFRunLoopTimerCreateWithHandler(
333 kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
334 [manager handlePress:event1
335 nextAction:^() {
336 key1Handled = false;
337 }];
338 });
339 CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer0, kCFRunLoopCommonModes);
340 CFRunLoopTimerRef timer1 = CFRunLoopTimerCreateWithHandler(
341 kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 1, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
342 // key1 should be completely finished by now
343 XCTAssertFalse(key1Handled);
344 [manager handlePress:event2
345 nextAction:^() {
346 key2Handled = false;
347 }];
348 // End the nested CFRunLoop
349 CFRunLoopStop(CFRunLoopGetCurrent());
350 });
351 CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer1, kCFRunLoopCommonModes);
352
353 // Add the callbacks to the CFRunLoop with mode kMessageLoopCFRunLoopMode
354 // This allows them to interrupt the loop started within handlePress
355 CFRunLoopTimerRef timer2 = CFRunLoopTimerCreateWithHandler(
356 kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 2, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
357 // No processing should be done on key2 yet
358 XCTAssertTrue(key1Callback != nil);
359 XCTAssertTrue(key2Callback == nil);
360 key1Callback(false);
361 });
362 CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer2,
363 fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
364 CFRunLoopTimerRef timer3 = CFRunLoopTimerCreateWithHandler(
365 kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 3, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
366 // Both keys should be processed by now
367 XCTAssertTrue(key1Callback != nil);
368 XCTAssertTrue(key2Callback != nil);
369 key2Callback(false);
370 });
371 CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer3,
372 fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
373
374 // Start a nested CFRunLoop so we can wait for both presses to complete before exiting the test
375 CFRunLoopRun();
376 XCTAssertFalse(key2Handled);
377 XCTAssertFalse(key1Handled);
378 }
379
380 @end
-[FlutterKeyboardManager addPrimaryResponder:]
void addPrimaryResponder:(nonnull id< FlutterKeyPrimaryResponder > responder)
Definition: FlutterKeyboardManager.mm:45
FlutterEnginePartialMock
Sometimes we have to use a custom mock to avoid retain cycles in ocmock.
Definition: FlutterKeyboardManagerTest.mm:27
FlutterEngine
Definition: FlutterEngine.h:61
FlutterBasicMessageChannel
Definition: FlutterChannels.h:37
-[FlutterKeyboardManager handlePress:nextAction:]
void handlePress:nextAction:(nonnull FlutterUIPressProxy *press,[nextAction] ios(13.4) API_AVAILABLE)
Definition: FlutterKeyboardManager.mm:71
FlutterViewController
Definition: FlutterViewController.h:56
FlutterKeyPrimaryResponder-p
Definition: FlutterKeyPrimaryResponder.h:19
flutter::testing::keyDownEvent
FlutterUIPressProxy * keyDownEvent(UIKeyboardHIDUsage keyCode, UIKeyModifierFlags modifierFlags=0x0, NSTimeInterval timestamp=0.0f, const char *characters="", const char *charactersIgnoringModifiers="") API_AVAILABLE(ios(13.4))
Definition: FlutterFakeKeyEvents.mm:90
API_AVAILABLE
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
-[FlutterKeyboardManager addSecondaryResponder:]
void addSecondaryResponder:(nonnull id< FlutterKeySecondaryResponder > responder)
Definition: FlutterKeyboardManager.mm:49
flutter::testing
Definition: FlutterFakeKeyEvents.h:51
FlutterMacros.h
viewController
FlutterViewController * viewController
Definition: FlutterTextInputPluginTest.mm:92
FLUTTER_ASSERT_ARC
FLUTTER_ASSERT_ARC
Definition: FlutterKeyboardManagerTest.mm:18
FlutterKeySecondaryResponder-p
Definition: FlutterKeySecondaryResponder.h:17
FlutterEngine(TestLowMemory)
Definition: FlutterKeyboardManagerTest.mm:40
-[FlutterKeyboardManagerUnittestsObjC nextResponderShouldThrowOnPressesEnded]
bool nextResponderShouldThrowOnPressesEnded()
-[FlutterKeyboardManagerUnittestsObjC emptyNextResponder]
bool emptyNextResponder()
FlutterFakeKeyEvents.h
FlutterAsyncKeyCallback
void(^ FlutterAsyncKeyCallback)(BOOL handled)
Definition: FlutterKeyPrimaryResponder.h:10
-[FlutterEngine(TestLowMemory) notifyLowMemory]
void notifyLowMemory()
flutter
Definition: accessibility_bridge.h:28
-[FlutterKeyboardManagerUnittestsObjC singlePrimaryResponder]
bool singlePrimaryResponder()
FlutterViewControllerWillDealloc
const NSNotificationName FlutterViewControllerWillDealloc
Definition: FlutterViewController.mm:43
FlutterKeyboardManager.h
flutter::PointerDataPacket
Definition: FlutterKeyboardManagerTest.mm:21
FlutterViewController_Internal.h
FlutterUIPressProxy
Definition: FlutterUIPressProxy.h:17
-[FlutterKeyboardManagerUnittestsObjC singleSecondaryResponder]
bool singleSecondaryResponder()
FlutterUIPressProxy.h
MockEngine
Definition: FlutterKeyboardManagerTest.mm:52
FlutterKeyboardManagerUnittestsObjC
Definition: FlutterKeyboardManagerTest.mm:55
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:53
-[FlutterKeyboardManagerUnittestsObjC doublePrimaryResponder]
bool doublePrimaryResponder()
FlutterKeyboardManagerTest
Definition: FlutterKeyboardManagerTest.mm:71