[go: nahoru, domu]

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"
17 
19 
20 namespace flutter {
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.
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 
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 
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 
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