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 namespace {
27 
28 typedef void (^KeyCallbackSetter)(FlutterUIPressProxy* press, FlutterAsyncKeyCallback callback)
29  API_AVAILABLE(ios(13.4));
30 typedef BOOL (^BoolGetter)();
31 
32 } // namespace
33 
34 // These tests were designed to run on iOS 13.4 or later.
35 API_AVAILABLE(ios(13.4))
36 @interface FlutterKeyboardManagerTest : XCTestCase
37 @end
38 
39 @implementation FlutterKeyboardManagerTest
40 
41 - (id<FlutterKeyPrimaryResponder>)mockPrimaryResponder:(KeyCallbackSetter)callbackSetter {
42  id<FlutterKeyPrimaryResponder> mock =
43  OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder));
44  OCMStub([mock handlePress:[OCMArg any] callback:[OCMArg any]])
45  .andDo((^(NSInvocation* invocation) {
46  __unsafe_unretained FlutterUIPressProxy* pressUnsafe;
47  __unsafe_unretained FlutterAsyncKeyCallback callbackUnsafe;
48 
49  [invocation getArgument:&pressUnsafe atIndex:2];
50  [invocation getArgument:&callbackUnsafe atIndex:3];
51 
52  // Retain the unretained parameters so they can
53  // be run in the perform block when this invocation goes out of scope.
54  FlutterUIPressProxy* press = pressUnsafe;
55  FlutterAsyncKeyCallback callback = callbackUnsafe;
56  CFRunLoopPerformBlock(CFRunLoopGetCurrent(),
57  fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, ^() {
58  callbackSetter(press, callback);
59  });
60  }));
61  return mock;
62 }
63 
64 - (id<FlutterKeySecondaryResponder>)mockSecondaryResponder:(BoolGetter)resultGetter {
65  id<FlutterKeySecondaryResponder> mock =
66  OCMStrictProtocolMock(@protocol(FlutterKeySecondaryResponder));
67  OCMStub([mock handlePress:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
68  BOOL result = resultGetter();
69  [invocation setReturnValue:&result];
70  }));
71  return mock;
72 }
73 
74 - (void)testNextResponderShouldThrowOnPressesEnded {
75  // The nextResponder is a strict mock and hasn't stubbed pressesEnded.
76  // An error will be thrown on pressesEnded.
77  UIResponder* nextResponder = OCMStrictClassMock([UIResponder class]);
78  OCMStub([nextResponder pressesBegan:OCMOCK_ANY withEvent:OCMOCK_ANY]);
79 
80  id mockEngine = OCMClassMock([FlutterEngine class]);
81  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
82  nibName:nil
83  bundle:nil];
84  FlutterViewController* owner = OCMPartialMock(viewController);
85  OCMStub([owner nextResponder]).andReturn(nextResponder);
86 
87  XCTAssertThrowsSpecificNamed([owner.nextResponder pressesEnded:[[NSSet alloc] init]
88  withEvent:[[UIPressesEvent alloc] init]],
89  NSException, NSInternalInconsistencyException);
90 
91  [mockEngine stopMocking];
92 }
93 
94 - (void)testSinglePrimaryResponder {
95  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
96  __block BOOL primaryResponse = FALSE;
97  __block int callbackCount = 0;
98  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
99  FlutterAsyncKeyCallback callback) {
100  callbackCount++;
101  callback(primaryResponse);
102  }]];
103  constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
104  // Case: The responder reports TRUE
105  __block bool completeHandled = true;
106  primaryResponse = TRUE;
107  [manager handlePress:keyDownEvent(keyId)
108  nextAction:^() {
109  completeHandled = false;
110  }];
111  XCTAssertEqual(callbackCount, 1);
112  XCTAssertTrue(completeHandled);
113  completeHandled = true;
114  callbackCount = 0;
115 
116  // Case: The responder reports FALSE
117  primaryResponse = FALSE;
118  [manager handlePress:keyUpEvent(keyId)
119  nextAction:^() {
120  completeHandled = false;
121  }];
122  XCTAssertEqual(callbackCount, 1);
123  XCTAssertFalse(completeHandled);
124 }
125 
126 - (void)testDoublePrimaryResponder {
127  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
128 
129  __block BOOL callback1Response = FALSE;
130  __block int callback1Count = 0;
131  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
132  FlutterAsyncKeyCallback callback) {
133  callback1Count++;
134  callback(callback1Response);
135  }]];
136 
137  __block BOOL callback2Response = FALSE;
138  __block int callback2Count = 0;
139  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
140  FlutterAsyncKeyCallback callback) {
141  callback2Count++;
142  callback(callback2Response);
143  }]];
144 
145  // Case: Both responders report TRUE.
146  __block bool somethingWasHandled = true;
147  constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
148  callback1Response = TRUE;
149  callback2Response = TRUE;
150  [manager handlePress:keyUpEvent(keyId)
151  nextAction:^() {
152  somethingWasHandled = false;
153  }];
154  XCTAssertEqual(callback1Count, 1);
155  XCTAssertEqual(callback2Count, 1);
156  XCTAssertTrue(somethingWasHandled);
157 
158  somethingWasHandled = true;
159  callback1Count = 0;
160  callback2Count = 0;
161 
162  // Case: One responder reports TRUE.
163  callback1Response = TRUE;
164  callback2Response = FALSE;
165  [manager handlePress:keyUpEvent(keyId)
166  nextAction:^() {
167  somethingWasHandled = false;
168  }];
169  XCTAssertEqual(callback1Count, 1);
170  XCTAssertEqual(callback2Count, 1);
171  XCTAssertTrue(somethingWasHandled);
172 
173  somethingWasHandled = true;
174  callback1Count = 0;
175  callback2Count = 0;
176 
177  // Case: Both responders report FALSE.
178  callback1Response = FALSE;
179  callback2Response = FALSE;
180  [manager handlePress:keyDownEvent(keyId)
181  nextAction:^() {
182  somethingWasHandled = false;
183  }];
184  XCTAssertEqual(callback1Count, 1);
185  XCTAssertEqual(callback2Count, 1);
186  XCTAssertFalse(somethingWasHandled);
187 }
188 
189 - (void)testSingleSecondaryResponder {
190  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
191 
192  __block BOOL primaryResponse = FALSE;
193  __block int callbackCount = 0;
194  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
195  FlutterAsyncKeyCallback callback) {
196  callbackCount++;
197  callback(primaryResponse);
198  }]];
199 
200  __block BOOL secondaryResponse;
201  [manager addSecondaryResponder:[self mockSecondaryResponder:^() {
202  return secondaryResponse;
203  }]];
204 
205  // Case: Primary responder responds TRUE. The event shouldn't be handled by
206  // the secondary responder.
207  constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
208  secondaryResponse = FALSE;
209  primaryResponse = TRUE;
210  __block bool completeHandled = true;
211  [manager handlePress:keyUpEvent(keyId)
212  nextAction:^() {
213  completeHandled = false;
214  }];
215  XCTAssertEqual(callbackCount, 1);
216  XCTAssertTrue(completeHandled);
217  completeHandled = true;
218  callbackCount = 0;
219 
220  // Case: Primary responder responds FALSE. The secondary responder returns
221  // TRUE.
222  secondaryResponse = TRUE;
223  primaryResponse = FALSE;
224  [manager handlePress:keyUpEvent(keyId)
225  nextAction:^() {
226  completeHandled = false;
227  }];
228  XCTAssertEqual(callbackCount, 1);
229  XCTAssertTrue(completeHandled);
230  completeHandled = true;
231  callbackCount = 0;
232 
233  // Case: Primary responder responds FALSE. The secondary responder returns FALSE.
234  secondaryResponse = FALSE;
235  primaryResponse = FALSE;
236  [manager handlePress:keyDownEvent(keyId)
237  nextAction:^() {
238  completeHandled = false;
239  }];
240  XCTAssertEqual(callbackCount, 1);
241  XCTAssertFalse(completeHandled);
242 }
243 
244 - (void)testEventsProcessedSequentially {
245  constexpr UIKeyboardHIDUsage keyId1 = (UIKeyboardHIDUsage)0x50;
246  constexpr UIKeyboardHIDUsage keyId2 = (UIKeyboardHIDUsage)0x51;
247  FlutterUIPressProxy* event1 = keyDownEvent(keyId1);
248  FlutterUIPressProxy* event2 = keyDownEvent(keyId2);
249  __block FlutterAsyncKeyCallback key1Callback;
250  __block FlutterAsyncKeyCallback key2Callback;
251  __block bool key1Handled = true;
252  __block bool key2Handled = true;
253 
254  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
255  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
256  FlutterAsyncKeyCallback callback) {
257  if (press == event1) {
258  key1Callback = callback;
259  } else if (press == event2) {
260  key2Callback = callback;
261  }
262  }]];
263 
264  // Add both presses into the main CFRunLoop queue
265  CFRunLoopTimerRef timer0 = CFRunLoopTimerCreateWithHandler(
266  kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
267  [manager handlePress:event1
268  nextAction:^() {
269  key1Handled = false;
270  }];
271  });
272  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer0, kCFRunLoopCommonModes);
273  CFRunLoopTimerRef timer1 = CFRunLoopTimerCreateWithHandler(
274  kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 1, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
275  // key1 should be completely finished by now
276  XCTAssertFalse(key1Handled);
277  [manager handlePress:event2
278  nextAction:^() {
279  key2Handled = false;
280  }];
281  // End the nested CFRunLoop
282  CFRunLoopStop(CFRunLoopGetCurrent());
283  });
284  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer1, kCFRunLoopCommonModes);
285 
286  // Add the callbacks to the CFRunLoop with mode kMessageLoopCFRunLoopMode
287  // This allows them to interrupt the loop started within handlePress
288  CFRunLoopTimerRef timer2 = CFRunLoopTimerCreateWithHandler(
289  kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 2, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
290  // No processing should be done on key2 yet
291  XCTAssertTrue(key1Callback != nil);
292  XCTAssertTrue(key2Callback == nil);
293  key1Callback(false);
294  });
295  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer2,
296  fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
297  CFRunLoopTimerRef timer3 = CFRunLoopTimerCreateWithHandler(
298  kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 3, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
299  // Both keys should be processed by now
300  XCTAssertTrue(key1Callback != nil);
301  XCTAssertTrue(key2Callback != nil);
302  key2Callback(false);
303  });
304  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer3,
305  fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
306 
307  // Start a nested CFRunLoop so we can wait for both presses to complete before exiting the test
308  CFRunLoopRun();
309  XCTAssertFalse(key2Handled);
310  XCTAssertFalse(key1Handled);
311 }
312 
313 @end
-[FlutterKeyboardManager addPrimaryResponder:]
void addPrimaryResponder:(nonnull id< FlutterKeyPrimaryResponder > responder)
Definition: FlutterKeyboardManager.mm:45
FlutterEngine
Definition: FlutterEngine.h:61
-[FlutterKeyboardManager handlePress:nextAction:]
void handlePress:nextAction:(nonnull FlutterUIPressProxy *press,[nextAction] ios(13.4) API_AVAILABLE)
Definition: FlutterKeyboardManager.mm:53
FlutterViewController
Definition: FlutterViewController.h:57
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
FlutterFakeKeyEvents.h
FlutterAsyncKeyCallback
void(^ FlutterAsyncKeyCallback)(BOOL handled)
Definition: FlutterKeyPrimaryResponder.h:10
flutter
Definition: accessibility_bridge.h:28
FlutterKeyboardManager.h
flutter::PointerDataPacket
Definition: FlutterKeyboardManagerTest.mm:21
FlutterViewController_Internal.h
FlutterUIPressProxy
Definition: FlutterUIPressProxy.h:17
FlutterUIPressProxy.h
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:53
FlutterKeyboardManagerTest
Definition: FlutterKeyboardManagerTest.mm:36