Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
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
19
20namespace flutter {
21class PointerDataPacket {};
22} // namespace flutter
23
24using namespace flutter::testing;
25
26namespace {
27
28typedef void (^KeyCallbackSetter)(FlutterUIPressProxy* press, FlutterAsyncKeyCallback callback)
29 API_AVAILABLE(ios(13.4));
30typedef BOOL (^BoolGetter)();
31
32} // namespace
33
34// These tests were designed to run on iOS 13.4 or later.
35API_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(),
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
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
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
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
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;
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
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,
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,
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
static CFStringRef kMessageLoopCFRunLoopMode
FlKeyEvent uint64_t FlKeyResponderAsyncCallback callback
GAsyncResult * result
id< FlutterKeyPrimaryResponder > mockPrimaryResponder:(KeyCallbackSetter callbackSetter)
id< FlutterKeySecondaryResponder > mockSecondaryResponder:(BoolGetter resultGetter)
void addPrimaryResponder:(nonnull id< FlutterKeyPrimaryResponder > responder)
void handlePress:nextAction:(nonnull FlutterUIPressProxy *press,[nextAction] ios(13.4) API_AVAILABLE)
void addSecondaryResponder:(nonnull id< FlutterKeySecondaryResponder > responder)
void(^ FlutterAsyncKeyCallback)(BOOL handled)
FlutterViewController * viewController
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
return FALSE
SK_API sk_sp< SkSurface > ios(9.0)
FlutterUIPressProxy * keyDownEvent(UIKeyboardHIDUsage keyCode, UIKeyModifierFlags modifierFlags=0x0, NSTimeInterval timestamp=0.0f, const char *characters="", const char *charactersIgnoringModifiers="") API_AVAILABLE(ios(13.4))
int BOOL