| 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/InternalFlutterSwift/InternalFlutterSwift.h"
14 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
15 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.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 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
FlutterFakeKeyEvents.h
FlutterAsyncKeyCallback
void(^ FlutterAsyncKeyCallback)(BOOL handled)
Definition: FlutterKeyPrimaryResponder.h:10
FlutterKeyboardManager.h
FLUTTER_ASSERT_ARC
FLUTTER_ASSERT_ARC
Definition: FlutterKeyboardManagerTest.mm:18
FlutterMacros.h
API_AVAILABLE
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
viewController
FlutterViewController * viewController
Definition: FlutterTextInputPluginTest.mm:140
FlutterViewController_Internal.h
flutter::PointerDataPacket
Definition: FlutterKeyboardManagerTest.mm:21
FlutterEngine
Definition: FlutterEngine.h:61
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:54
-[FlutterKeyboardManager addPrimaryResponder:]
void addPrimaryResponder:(nonnull id< FlutterKeyPrimaryResponder > responder)
Definition: FlutterKeyboardManager.mm:45
-[FlutterKeyboardManager handlePress:nextAction:]
void handlePress:nextAction:(nonnull FlutterUIPressProxy *press,[nextAction] ios(13.4) API_AVAILABLE)
Definition: FlutterKeyboardManager.mm:53
-[FlutterKeyboardManager addSecondaryResponder:]
void addSecondaryResponder:(nonnull id< FlutterKeySecondaryResponder > responder)
Definition: FlutterKeyboardManager.mm:49
FlutterKeyboardManagerTest
Definition: FlutterKeyboardManagerTest.mm:37
FlutterViewController
Definition: FlutterViewController.h:57
flutter::testing
Definition: FlutterFakeKeyEvents.h:33
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:59
flutter
Definition: accessibility_bridge.h:26
FlutterKeyPrimaryResponder-p
Definition: FlutterKeyPrimaryResponder.h:19
FlutterKeySecondaryResponder-p
Definition: FlutterKeySecondaryResponder.h:17