@onekeyfe/react-native-pager-view 3.0.157-alpha.257 → 3.0.157-alpha.262
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -4
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +295 -59
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +15 -11
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +600 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +145 -0
- package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
- package/android/src/main/jni/CMakeLists.txt +19 -0
- package/android/src/main/jni/pagerview.h +11 -0
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +1 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +32 -52
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
- package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
- package/docs/SPEC.md +282 -34
- package/ios/RNCCollapsiblePagerViewComponentView.mm +187 -1
- package/ios/RNCNativeScrollerComponentView.h +17 -0
- package/ios/RNCNativeScrollerComponentView.mm +479 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +759 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
- package/ios/tests/RNCNativeScrollerTests.mm +252 -0
- package/lib/module/CollapsiblePagerView.js +10 -1
- package/lib/module/NativeScroller.js +171 -11
- package/lib/module/NativeScrollerNativeComponent.ts +101 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -3
- package/lib/typescript/src/NativeScroller.d.ts +15 -7
- package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
- package/package.json +7 -4
- package/react-native-pager-view.podspec +2 -1
- package/react-native.config.js +15 -0
- package/src/CollapsiblePagerView.tsx +14 -4
- package/src/NativeScroller.tsx +291 -14
- package/src/NativeScrollerNativeComponent.ts +101 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
#import <XCTest/XCTest.h>
|
|
2
|
+
#import <UIKit/UIGestureRecognizerSubclass.h>
|
|
3
|
+
#import <React/RCTSurfaceTouchHandler.h>
|
|
4
|
+
#import <React/RCTViewComponentView.h>
|
|
5
|
+
#import <dlfcn.h>
|
|
6
|
+
#import <mach/mach_time.h>
|
|
7
|
+
#import <objc/message.h>
|
|
8
|
+
|
|
9
|
+
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
10
|
+
#import <react/renderer/components/pagerview/Props.h>
|
|
11
|
+
|
|
12
|
+
using namespace facebook::react;
|
|
13
|
+
|
|
14
|
+
#pragma mark - UIKit gesture environment and touch delivery
|
|
15
|
+
|
|
16
|
+
// Pod unit tests run in xctest without an application. UIKit then has no
|
|
17
|
+
// gesture environment: every programmatic recognizer state change, including
|
|
18
|
+
// Began on a plain UIGestureRecognizer, is dropped. Create the application
|
|
19
|
+
// singleton (what UIApplicationMain does first) so recognizers run under the
|
|
20
|
+
// real UIGestureEnvironment. Touches below are delivered through
|
|
21
|
+
// -[UIWindow sendEvent:], the path UIApplication uses for touch events, using
|
|
22
|
+
// the same private UITouch/UIEvent/IOHIDEvent calls as KIF. These are
|
|
23
|
+
// test-only; a missing symbol fails the test instead of skipping it.
|
|
24
|
+
@interface RNCPressCancellationTestApplication : UIApplication
|
|
25
|
+
@end
|
|
26
|
+
|
|
27
|
+
@implementation RNCPressCancellationTestApplication
|
|
28
|
+
// UIApplicationMain never ran, so there is no event run loop. UIScrollView
|
|
29
|
+
// pushes a tracking run-loop mode when its real pan begins; scheduling is
|
|
30
|
+
// outside the recognizer semantics under test.
|
|
31
|
+
- (void)_pushRunLoopMode:(id)mode requester:(id)requester reason:(id)reason {}
|
|
32
|
+
- (void)_popRunLoopMode:(id)mode requester:(id)requester reason:(id)reason {}
|
|
33
|
+
@end
|
|
34
|
+
|
|
35
|
+
static BOOL RNCEnsureUIKitGestureEnvironment(void)
|
|
36
|
+
{
|
|
37
|
+
if (UIApplication.sharedApplication != nil) return YES;
|
|
38
|
+
void (*instantiate)(Class) =
|
|
39
|
+
(void (*)(Class))dlsym(RTLD_DEFAULT, "UIApplicationInstantiateSingleton");
|
|
40
|
+
if (instantiate == NULL) return NO;
|
|
41
|
+
instantiate(RNCPressCancellationTestApplication.class);
|
|
42
|
+
return UIApplication.sharedApplication != nil;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
typedef struct __IOHIDEvent *RNCTestHIDEventRef;
|
|
46
|
+
|
|
47
|
+
@interface UITouch (RNCPressCancellationSynthesis)
|
|
48
|
+
- (void)setWindow:(UIWindow *)window;
|
|
49
|
+
- (void)setView:(UIView *)view;
|
|
50
|
+
- (void)setGestureView:(UIView *)view;
|
|
51
|
+
- (void)setPhase:(UITouchPhase)phase;
|
|
52
|
+
- (void)setTimestamp:(NSTimeInterval)timestamp;
|
|
53
|
+
- (void)setTapCount:(NSUInteger)tapCount;
|
|
54
|
+
- (void)_setLocationInWindow:(CGPoint)location resetPrevious:(BOOL)resetPrevious;
|
|
55
|
+
- (void)_setIsTapToClick:(BOOL)isTapToClick;
|
|
56
|
+
- (void)_setHidEvent:(RNCTestHIDEventRef)event;
|
|
57
|
+
@end
|
|
58
|
+
|
|
59
|
+
@interface UIEvent (RNCPressCancellationSynthesis)
|
|
60
|
+
- (instancetype)_init;
|
|
61
|
+
- (void)_clearTouches;
|
|
62
|
+
- (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)delayed;
|
|
63
|
+
- (void)_setHIDEvent:(RNCTestHIDEventRef)event;
|
|
64
|
+
@end
|
|
65
|
+
|
|
66
|
+
// Delivers one finger or several simultaneous fingers through a window.
|
|
67
|
+
@interface RNCSynthesizedTouchDriver : NSObject
|
|
68
|
+
- (instancetype)initWithWindow:(UIWindow *)window;
|
|
69
|
+
@property (nonatomic, readonly) NSString *unavailableReason;
|
|
70
|
+
- (UITouch *)beginAt:(CGPoint)windowPoint;
|
|
71
|
+
- (void)move:(UITouch *)touch to:(CGPoint)windowPoint;
|
|
72
|
+
- (void)end:(UITouch *)touch;
|
|
73
|
+
@end
|
|
74
|
+
|
|
75
|
+
@implementation RNCSynthesizedTouchDriver {
|
|
76
|
+
UIWindow *_window;
|
|
77
|
+
UIEvent *_event;
|
|
78
|
+
NSMutableArray<UITouch *> *_activeTouches;
|
|
79
|
+
NSTimeInterval _timestamp;
|
|
80
|
+
RNCTestHIDEventRef (*_createHand)(CFAllocatorRef, uint64_t, uint32_t, uint32_t, uint32_t,
|
|
81
|
+
uint32_t, uint32_t, double, double, double, double, double,
|
|
82
|
+
Boolean, Boolean, uint32_t);
|
|
83
|
+
RNCTestHIDEventRef (*_createFinger)(CFAllocatorRef, uint64_t, uint32_t, uint32_t, uint32_t,
|
|
84
|
+
double, double, double, double, double, double, double,
|
|
85
|
+
double, double, double, Boolean, Boolean, uint32_t);
|
|
86
|
+
void (*_setIntegerValue)(RNCTestHIDEventRef, uint32_t, long, uint32_t);
|
|
87
|
+
void (*_appendEvent)(RNCTestHIDEventRef, RNCTestHIDEventRef, uint32_t);
|
|
88
|
+
void (*_setSenderID)(RNCTestHIDEventRef, uint64_t);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
- (instancetype)initWithWindow:(UIWindow *)window
|
|
92
|
+
{
|
|
93
|
+
if (self = [super init]) {
|
|
94
|
+
_window = window;
|
|
95
|
+
_activeTouches = [NSMutableArray new];
|
|
96
|
+
_timestamp = NSProcessInfo.processInfo.systemUptime;
|
|
97
|
+
void *iokit = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW);
|
|
98
|
+
_createHand = (decltype(_createHand))dlsym(iokit, "IOHIDEventCreateDigitizerEvent");
|
|
99
|
+
_createFinger =
|
|
100
|
+
(decltype(_createFinger))dlsym(iokit, "IOHIDEventCreateDigitizerFingerEventWithQuality");
|
|
101
|
+
_setIntegerValue =
|
|
102
|
+
(decltype(_setIntegerValue))dlsym(iokit, "IOHIDEventSetIntegerValueWithOptions");
|
|
103
|
+
_appendEvent = (decltype(_appendEvent))dlsym(iokit, "IOHIDEventAppendEvent");
|
|
104
|
+
_setSenderID = (decltype(_setSenderID))dlsym(iokit, "IOHIDEventSetSenderID");
|
|
105
|
+
Class eventClass = NSClassFromString(@"UITouchesEvent");
|
|
106
|
+
if (!RNCEnsureUIKitGestureEnvironment()) {
|
|
107
|
+
_unavailableReason = @"UIApplication singleton unavailable";
|
|
108
|
+
} else if (_createHand == NULL || _createFinger == NULL || _setIntegerValue == NULL ||
|
|
109
|
+
_appendEvent == NULL || _setSenderID == NULL) {
|
|
110
|
+
_unavailableReason = @"IOHIDEvent symbols unavailable";
|
|
111
|
+
} else if (eventClass == Nil || ![eventClass instancesRespondToSelector:@selector(_init)] ||
|
|
112
|
+
![eventClass instancesRespondToSelector:@selector(_addTouch:forDelayedDelivery:)] ||
|
|
113
|
+
![UITouch instancesRespondToSelector:@selector(_setLocationInWindow:resetPrevious:)] ||
|
|
114
|
+
![UITouch instancesRespondToSelector:@selector(_setHidEvent:)]) {
|
|
115
|
+
_unavailableReason = @"UITouch/UITouchesEvent synthesis selectors unavailable";
|
|
116
|
+
} else {
|
|
117
|
+
_event = [[eventClass alloc] _init];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return self;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
- (RNCTestHIDEventRef)newHIDEvent
|
|
124
|
+
{
|
|
125
|
+
static const uint32_t kDigitizerDisplayIntegrated = (11 << 16) | 25;
|
|
126
|
+
static const uint32_t kIsBuiltIn = 4;
|
|
127
|
+
static const uint32_t kOptions = (uint32_t)-268435456;
|
|
128
|
+
uint64_t machTime = mach_absolute_time();
|
|
129
|
+
// Hand transducer, touch mask.
|
|
130
|
+
RNCTestHIDEventRef hand =
|
|
131
|
+
_createHand(kCFAllocatorDefault, machTime, 3, 0, 0, 1 << 1, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
132
|
+
_setIntegerValue(hand, kDigitizerDisplayIntegrated, 1, kOptions);
|
|
133
|
+
_setIntegerValue(hand, kIsBuiltIn, 1, kOptions);
|
|
134
|
+
_setSenderID(hand, 0x000000010000027F);
|
|
135
|
+
uint32_t index = 0;
|
|
136
|
+
for (UITouch *touch in _activeTouches) {
|
|
137
|
+
UITouchPhase phase = touch.phase;
|
|
138
|
+
// Position for moves; range and touch for contact changes.
|
|
139
|
+
uint32_t mask = phase == UITouchPhaseMoved ? (1 << 2) : ((1 << 0) | (1 << 1));
|
|
140
|
+
Boolean down = phase != UITouchPhaseEnded && phase != UITouchPhaseCancelled;
|
|
141
|
+
CGPoint point = [touch locationInView:_window];
|
|
142
|
+
RNCTestHIDEventRef finger = _createFinger(kCFAllocatorDefault, machTime, ++index, 2, mask,
|
|
143
|
+
point.x, point.y, 0, 0, 0, 5, 5, 1, 1, 1,
|
|
144
|
+
down, down, 0);
|
|
145
|
+
_setIntegerValue(finger, kDigitizerDisplayIntegrated, 1, kOptions);
|
|
146
|
+
_appendEvent(hand, finger, 0);
|
|
147
|
+
CFRelease(finger);
|
|
148
|
+
}
|
|
149
|
+
return hand;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
- (void)sendChangingTouch:(UITouch *)changed phase:(UITouchPhase)phase
|
|
153
|
+
{
|
|
154
|
+
NSAssert(_event != nil, @"%@", _unavailableReason);
|
|
155
|
+
_timestamp += 1.0 / 60.0;
|
|
156
|
+
for (UITouch *touch in _activeTouches) {
|
|
157
|
+
[touch setPhase:touch == changed ? phase : UITouchPhaseStationary];
|
|
158
|
+
[touch setTimestamp:_timestamp];
|
|
159
|
+
}
|
|
160
|
+
RNCTestHIDEventRef hid = [self newHIDEvent];
|
|
161
|
+
for (UITouch *touch in _activeTouches) [touch _setHidEvent:hid];
|
|
162
|
+
[_event _setHIDEvent:hid];
|
|
163
|
+
if (phase == UITouchPhaseBegan) [_event _addTouch:changed forDelayedDelivery:NO];
|
|
164
|
+
CFRelease(hid);
|
|
165
|
+
[_window sendEvent:_event];
|
|
166
|
+
if (phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled) {
|
|
167
|
+
[_activeTouches removeObjectIdenticalTo:changed];
|
|
168
|
+
if (_activeTouches.count == 0) [_event _clearTouches];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
- (UITouch *)beginAt:(CGPoint)windowPoint
|
|
173
|
+
{
|
|
174
|
+
if (_activeTouches.count == 0) [_event _clearTouches];
|
|
175
|
+
UITouch *touch = [UITouch new];
|
|
176
|
+
UIView *view = [_window hitTest:windowPoint withEvent:nil];
|
|
177
|
+
[touch setWindow:_window];
|
|
178
|
+
[touch _setLocationInWindow:windowPoint resetPrevious:YES];
|
|
179
|
+
[touch setView:view];
|
|
180
|
+
if ([touch respondsToSelector:@selector(setGestureView:)]) [touch setGestureView:view];
|
|
181
|
+
[touch _setIsTapToClick:NO];
|
|
182
|
+
[touch setTapCount:1];
|
|
183
|
+
[_activeTouches addObject:touch];
|
|
184
|
+
[self sendChangingTouch:touch phase:UITouchPhaseBegan];
|
|
185
|
+
return touch;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
- (void)move:(UITouch *)touch to:(CGPoint)windowPoint
|
|
189
|
+
{
|
|
190
|
+
[touch _setLocationInWindow:windowPoint resetPrevious:NO];
|
|
191
|
+
[self sendChangingTouch:touch phase:UITouchPhaseMoved];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
- (void)end:(UITouch *)touch
|
|
195
|
+
{
|
|
196
|
+
[self sendChangingTouch:touch phase:UITouchPhaseEnded];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@end
|
|
200
|
+
|
|
201
|
+
// Private declarations access the production class compiled into the pod.
|
|
202
|
+
@interface RNCCollapsiblePagerContentPressCancellationGestureRecognizer : UIGestureRecognizer
|
|
203
|
+
@property (nonatomic, weak) UIGestureRecognizer *reactTouchHandler;
|
|
204
|
+
- (void)cancelPressForPan:(UIPanGestureRecognizer *)pan;
|
|
205
|
+
- (void)observeIncomingTouch:(UITouch *)touch;
|
|
206
|
+
@end
|
|
207
|
+
|
|
208
|
+
// Terminal states are reset by UIKit once it processes them. Observe requests
|
|
209
|
+
// while retaining the production implementation and UIKit setter.
|
|
210
|
+
@interface RNCPressCancellationObservedBridge : RNCCollapsiblePagerContentPressCancellationGestureRecognizer
|
|
211
|
+
@property (nonatomic, strong) NSMutableArray<NSNumber *> *requestedStates;
|
|
212
|
+
@end
|
|
213
|
+
@implementation RNCPressCancellationObservedBridge
|
|
214
|
+
- (void)setState:(UIGestureRecognizerState)state
|
|
215
|
+
{
|
|
216
|
+
if (self.requestedStates == nil) self.requestedStates = [NSMutableArray new];
|
|
217
|
+
[self.requestedStates addObject:@(state)];
|
|
218
|
+
[super setState:state];
|
|
219
|
+
}
|
|
220
|
+
@end
|
|
221
|
+
|
|
222
|
+
// These doubles supply input facts to the bridge's decision logic directly.
|
|
223
|
+
// The production bridge owns its state transitions. End-to-end UIKit delivery
|
|
224
|
+
// through the real pager is covered by the delivery suite below.
|
|
225
|
+
@interface RNCPressCancellationTestTouch : UITouch
|
|
226
|
+
@property (nonatomic, strong) UIView *testView;
|
|
227
|
+
@property (nonatomic, assign) UITouchPhase testPhase;
|
|
228
|
+
@end
|
|
229
|
+
@implementation RNCPressCancellationTestTouch
|
|
230
|
+
- (UIView *)view { return self.testView; }
|
|
231
|
+
- (UITouchPhase)phase { return self.testPhase; }
|
|
232
|
+
@end
|
|
233
|
+
|
|
234
|
+
@interface RNCPressCancellationTestEvent : UIEvent
|
|
235
|
+
@property (nonatomic, copy) NSSet<UITouch *> *testTouches;
|
|
236
|
+
@end
|
|
237
|
+
@implementation RNCPressCancellationTestEvent
|
|
238
|
+
- (NSSet<UITouch *> *)allTouches { return self.testTouches; }
|
|
239
|
+
@end
|
|
240
|
+
|
|
241
|
+
@interface RNCPressCancellationTestPan : UIPanGestureRecognizer
|
|
242
|
+
@property (nonatomic, assign) UIGestureRecognizerState testState;
|
|
243
|
+
@end
|
|
244
|
+
@implementation RNCPressCancellationTestPan
|
|
245
|
+
- (UIGestureRecognizerState)state { return self.testState; }
|
|
246
|
+
@end
|
|
247
|
+
|
|
248
|
+
@interface RNCCollapsiblePagerPressCancellationTests : XCTestCase
|
|
249
|
+
@property (nonatomic, strong) UIWindow *window;
|
|
250
|
+
@property (nonatomic, strong) UIView *host;
|
|
251
|
+
@property (nonatomic, strong) UIView *surface;
|
|
252
|
+
@property (nonatomic, strong) UIView *content;
|
|
253
|
+
@property (nonatomic, strong) RCTSurfaceTouchHandler *handler;
|
|
254
|
+
@property (nonatomic, strong) RNCPressCancellationObservedBridge *bridge;
|
|
255
|
+
@property (nonatomic, strong) RNCPressCancellationTestPan *pan;
|
|
256
|
+
@property (nonatomic, strong) RNCPressCancellationTestTouch *touch;
|
|
257
|
+
@end
|
|
258
|
+
|
|
259
|
+
@implementation RNCCollapsiblePagerPressCancellationTests
|
|
260
|
+
|
|
261
|
+
- (void)setUp
|
|
262
|
+
{
|
|
263
|
+
[super setUp];
|
|
264
|
+
XCTAssertTrue(NSThread.isMainThread);
|
|
265
|
+
// Without an application UIKit drops every programmatic recognizer state.
|
|
266
|
+
XCTAssertTrue(RNCEnsureUIKitGestureEnvironment());
|
|
267
|
+
// Register the recognizers with a UIKit gesture environment without taking focus.
|
|
268
|
+
self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
269
|
+
self.window.hidden = NO;
|
|
270
|
+
self.host = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
271
|
+
self.surface = [[UIView alloc] initWithFrame:self.host.bounds];
|
|
272
|
+
self.content = [[UIView alloc] initWithFrame:self.surface.bounds];
|
|
273
|
+
[self.window addSubview:self.host];
|
|
274
|
+
[self.host addSubview:self.surface];
|
|
275
|
+
[self.surface addSubview:self.content];
|
|
276
|
+
self.handler = [RCTSurfaceTouchHandler new];
|
|
277
|
+
[self.handler attachToView:self.surface];
|
|
278
|
+
Class bridgeClass = NSClassFromString(@"RNCCollapsiblePagerContentPressCancellationGestureRecognizer");
|
|
279
|
+
XCTAssertNotNil(bridgeClass);
|
|
280
|
+
self.bridge = [RNCPressCancellationObservedBridge new];
|
|
281
|
+
self.bridge.reactTouchHandler = self.handler;
|
|
282
|
+
[self.host addGestureRecognizer:self.bridge];
|
|
283
|
+
XCTAssertEqual(self.bridge.view.window, self.window);
|
|
284
|
+
XCTAssertEqual(self.handler.view.window, self.window);
|
|
285
|
+
self.pan = [RNCPressCancellationTestPan new];
|
|
286
|
+
self.pan.testState = UIGestureRecognizerStateBegan;
|
|
287
|
+
self.touch = [self touchInView:self.content phase:UITouchPhaseBegan];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
- (void)tearDown
|
|
291
|
+
{
|
|
292
|
+
[self.host removeGestureRecognizer:self.bridge];
|
|
293
|
+
[self.handler detachFromView:self.surface];
|
|
294
|
+
self.bridge = nil;
|
|
295
|
+
self.handler = nil;
|
|
296
|
+
self.touch = nil;
|
|
297
|
+
self.pan = nil;
|
|
298
|
+
self.content = nil;
|
|
299
|
+
self.surface = nil;
|
|
300
|
+
[self.host removeFromSuperview];
|
|
301
|
+
self.host = nil;
|
|
302
|
+
self.window.hidden = YES;
|
|
303
|
+
self.window = nil;
|
|
304
|
+
[super tearDown];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
- (RNCPressCancellationTestTouch *)touchInView:(UIView *)view phase:(UITouchPhase)phase
|
|
308
|
+
{
|
|
309
|
+
RNCPressCancellationTestTouch *touch = [RNCPressCancellationTestTouch new];
|
|
310
|
+
touch.testView = view;
|
|
311
|
+
touch.testPhase = phase;
|
|
312
|
+
return touch;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
- (RNCPressCancellationTestEvent *)eventWithTouches:(NSSet<UITouch *> *)touches
|
|
316
|
+
{
|
|
317
|
+
RNCPressCancellationTestEvent *event = [RNCPressCancellationTestEvent new];
|
|
318
|
+
event.testTouches = touches;
|
|
319
|
+
return event;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
- (void)beginSingleTouch
|
|
323
|
+
{
|
|
324
|
+
[self.bridge touchesBegan:[NSSet setWithObject:self.touch]
|
|
325
|
+
withEvent:[self eventWithTouches:[NSSet setWithObject:self.touch]]];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
- (void)testPreventionTargetsExactHandlerAndUsesExternalHost
|
|
329
|
+
{
|
|
330
|
+
RCTSurfaceTouchHandler *otherHandler = [RCTSurfaceTouchHandler new];
|
|
331
|
+
UIPanGestureRecognizer *nativePan = [UIPanGestureRecognizer new];
|
|
332
|
+
XCTAssertTrue([self.bridge canPreventGestureRecognizer:self.handler]);
|
|
333
|
+
XCTAssertFalse([self.bridge canPreventGestureRecognizer:otherHandler]);
|
|
334
|
+
XCTAssertFalse([self.bridge canPreventGestureRecognizer:nativePan]);
|
|
335
|
+
XCTAssertTrue([self.handler canBePreventedByGestureRecognizer:self.bridge]);
|
|
336
|
+
|
|
337
|
+
[self.content addGestureRecognizer:nativePan];
|
|
338
|
+
XCTAssertFalse([self.handler canBePreventedByGestureRecognizer:nativePan]);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
- (void)testOnlyBeganPanWithTrackedLiveTouchActivates
|
|
342
|
+
{
|
|
343
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
344
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
345
|
+
[self beginSingleTouch];
|
|
346
|
+
for (NSNumber *state in @[@(UIGestureRecognizerStatePossible),
|
|
347
|
+
@(UIGestureRecognizerStateChanged),
|
|
348
|
+
@(UIGestureRecognizerStateFailed),
|
|
349
|
+
@(UIGestureRecognizerStateEnded)]) {
|
|
350
|
+
self.pan.testState = (UIGestureRecognizerState)state.integerValue;
|
|
351
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
352
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
353
|
+
}
|
|
354
|
+
self.pan.testState = UIGestureRecognizerStateBegan;
|
|
355
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
356
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
357
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
358
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
- (void)testEndedAndCancelledOriginTouchesCannotActivate
|
|
362
|
+
{
|
|
363
|
+
[self beginSingleTouch];
|
|
364
|
+
for (NSNumber *phase in @[@(UITouchPhaseEnded), @(UITouchPhaseCancelled)]) {
|
|
365
|
+
self.touch.testPhase = (UITouchPhase)phase.integerValue;
|
|
366
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
367
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
- (void)testSecondActiveTouchOutsidePagerButOnSameSurfaceBlocksCancellation
|
|
372
|
+
{
|
|
373
|
+
UIView *sibling = [UIView new];
|
|
374
|
+
[self.surface addSubview:sibling];
|
|
375
|
+
RNCPressCancellationTestTouch *second = [self touchInView:sibling phase:UITouchPhaseStationary];
|
|
376
|
+
[self beginSingleTouch];
|
|
377
|
+
[self.bridge touchesMoved:[NSSet setWithObject:self.touch]
|
|
378
|
+
withEvent:[self eventWithTouches:[NSSet setWithObjects:self.touch, second, nil]]];
|
|
379
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
380
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
- (void)testOtherSurfaceAndFinishedTouchesDoNotCountAsActiveSurfaceTouches
|
|
384
|
+
{
|
|
385
|
+
UIView *otherSurface = [UIView new];
|
|
386
|
+
[self.host addSubview:otherSurface];
|
|
387
|
+
RNCPressCancellationTestTouch *other = [self touchInView:otherSurface phase:UITouchPhaseMoved];
|
|
388
|
+
RNCPressCancellationTestTouch *ended = [self touchInView:self.surface phase:UITouchPhaseEnded];
|
|
389
|
+
RNCPressCancellationTestTouch *cancelled = [self touchInView:self.content phase:UITouchPhaseCancelled];
|
|
390
|
+
[self.bridge touchesBegan:[NSSet setWithObject:self.touch]
|
|
391
|
+
withEvent:[self eventWithTouches:[NSSet setWithObjects:self.touch, other, ended, cancelled, nil]]];
|
|
392
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
393
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
394
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
395
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
- (void)testMultipleOriginTouchesFailWithoutRecognizing
|
|
399
|
+
{
|
|
400
|
+
RNCPressCancellationTestTouch *second = [self touchInView:self.content phase:UITouchPhaseBegan];
|
|
401
|
+
NSSet<UITouch *> *touches = [NSSet setWithObjects:self.touch, second, nil];
|
|
402
|
+
[self.bridge touchesBegan:touches withEvent:[self eventWithTouches:touches]];
|
|
403
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateFailed)]);
|
|
404
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
405
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
406
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
- (void)testSecondTouchAfterRecognitionDoesNotFailOrEndTheTrackedGesture
|
|
410
|
+
{
|
|
411
|
+
[self beginSingleTouch];
|
|
412
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
413
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
414
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
415
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
416
|
+
RNCPressCancellationTestTouch *second = [self touchInView:self.content phase:UITouchPhaseBegan];
|
|
417
|
+
RNCPressCancellationTestEvent *event =
|
|
418
|
+
[self eventWithTouches:[NSSet setWithObjects:self.touch, second, nil]];
|
|
419
|
+
NSUInteger requestCount = self.bridge.requestedStates.count;
|
|
420
|
+
XCTAssertNoThrow([self.bridge touchesBegan:[NSSet setWithObject:second] withEvent:event]);
|
|
421
|
+
XCTAssertEqual(self.bridge.requestedStates.count, requestCount);
|
|
422
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
423
|
+
second.testPhase = UITouchPhaseEnded;
|
|
424
|
+
[self.bridge touchesEnded:[NSSet setWithObject:second] withEvent:event];
|
|
425
|
+
XCTAssertEqual(self.bridge.requestedStates.count, requestCount);
|
|
426
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
427
|
+
self.touch.testPhase = UITouchPhaseEnded;
|
|
428
|
+
[self.bridge touchesEnded:[NSSet setWithObject:self.touch] withEvent:event];
|
|
429
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateEnded)]);
|
|
430
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
- (void)testSecondTouchCancellationDoesNotCancelTheTrackedGesture
|
|
434
|
+
{
|
|
435
|
+
[self beginSingleTouch];
|
|
436
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
437
|
+
RNCPressCancellationTestTouch *second = [self touchInView:self.content phase:UITouchPhaseBegan];
|
|
438
|
+
RNCPressCancellationTestEvent *event =
|
|
439
|
+
[self eventWithTouches:[NSSet setWithObjects:self.touch, second, nil]];
|
|
440
|
+
[self.bridge touchesBegan:[NSSet setWithObject:second] withEvent:event];
|
|
441
|
+
second.testPhase = UITouchPhaseCancelled;
|
|
442
|
+
[self.bridge touchesCancelled:[NSSet setWithObject:second] withEvent:event];
|
|
443
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
444
|
+
self.touch.testPhase = UITouchPhaseCancelled;
|
|
445
|
+
[self.bridge touchesCancelled:[NSSet setWithObject:self.touch] withEvent:event];
|
|
446
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateCancelled)]);
|
|
447
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
- (void)testRejectedSameSurfaceTouchRemainsLatchedUntilReset
|
|
451
|
+
{
|
|
452
|
+
UIView *sibling = [UIView new];
|
|
453
|
+
[self.surface addSubview:sibling];
|
|
454
|
+
RNCPressCancellationTestTouch *second = [self touchInView:sibling phase:UITouchPhaseBegan];
|
|
455
|
+
[self beginSingleTouch];
|
|
456
|
+
// The pager delegate observes this touch even when it rejects delivery to the bridge.
|
|
457
|
+
[self.bridge observeIncomingTouch:second];
|
|
458
|
+
second.testPhase = UITouchPhaseEnded;
|
|
459
|
+
[self.bridge touchesMoved:[NSSet setWithObject:self.touch]
|
|
460
|
+
withEvent:[self eventWithTouches:[NSSet setWithObject:self.touch]]];
|
|
461
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
462
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
463
|
+
[self.bridge reset];
|
|
464
|
+
[self beginSingleTouch];
|
|
465
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
466
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
467
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
468
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
- (void)testObservedOtherSurfaceTouchDoesNotLatchCurrentSurface
|
|
472
|
+
{
|
|
473
|
+
UIView *otherSurface = [UIView new];
|
|
474
|
+
[self.host addSubview:otherSurface];
|
|
475
|
+
RNCPressCancellationTestTouch *second = [self touchInView:otherSurface phase:UITouchPhaseBegan];
|
|
476
|
+
[self beginSingleTouch];
|
|
477
|
+
[self.bridge observeIncomingTouch:second];
|
|
478
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
479
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
480
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
481
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
- (void)testHostMismatchDoesNotCancel
|
|
485
|
+
{
|
|
486
|
+
[self.host removeGestureRecognizer:self.bridge];
|
|
487
|
+
[self.surface addGestureRecognizer:self.bridge];
|
|
488
|
+
[self beginSingleTouch];
|
|
489
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
490
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
491
|
+
[self.surface removeGestureRecognizer:self.bridge];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
- (void)testResetDropsPendingTouchAndAllowsANewSequence
|
|
495
|
+
{
|
|
496
|
+
[self beginSingleTouch];
|
|
497
|
+
[self.bridge reset];
|
|
498
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
499
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
500
|
+
[self beginSingleTouch];
|
|
501
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
502
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
503
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
504
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
- (void)testRecognizedSequenceEndsAndReleasesEvent
|
|
508
|
+
{
|
|
509
|
+
__weak UIEvent *weakEvent;
|
|
510
|
+
@autoreleasepool {
|
|
511
|
+
RNCPressCancellationTestEvent *event = [self eventWithTouches:[NSSet setWithObject:self.touch]];
|
|
512
|
+
weakEvent = event;
|
|
513
|
+
[self.bridge touchesBegan:[NSSet setWithObject:self.touch] withEvent:event];
|
|
514
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
515
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
516
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
517
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
518
|
+
self.touch.testPhase = UITouchPhaseEnded;
|
|
519
|
+
[self.bridge touchesEnded:[NSSet setWithObject:self.touch] withEvent:event];
|
|
520
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateEnded)]);
|
|
521
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
522
|
+
}
|
|
523
|
+
XCTAssertNil(weakEvent);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
- (void)testUnrecognizedCancellationFailsAndReleasesEvent
|
|
527
|
+
{
|
|
528
|
+
__weak UIEvent *weakEvent;
|
|
529
|
+
@autoreleasepool {
|
|
530
|
+
RNCPressCancellationTestEvent *event = [self eventWithTouches:[NSSet setWithObject:self.touch]];
|
|
531
|
+
weakEvent = event;
|
|
532
|
+
[self.bridge touchesBegan:[NSSet setWithObject:self.touch] withEvent:event];
|
|
533
|
+
self.touch.testPhase = UITouchPhaseCancelled;
|
|
534
|
+
[self.bridge touchesCancelled:[NSSet setWithObject:self.touch] withEvent:event];
|
|
535
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateFailed)]);
|
|
536
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
537
|
+
}
|
|
538
|
+
XCTAssertNil(weakEvent);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
- (void)testRecognizedCancellationTerminatesTheBridge
|
|
542
|
+
{
|
|
543
|
+
[self beginSingleTouch];
|
|
544
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
545
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
546
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
547
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
548
|
+
self.touch.testPhase = UITouchPhaseCancelled;
|
|
549
|
+
[self.bridge touchesCancelled:[NSSet setWithObject:self.touch]
|
|
550
|
+
withEvent:[self eventWithTouches:[NSSet setWithObject:self.touch]]];
|
|
551
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateCancelled)]);
|
|
552
|
+
XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
- (void)testHandlerBindingIsWeak
|
|
556
|
+
{
|
|
557
|
+
__weak UIGestureRecognizer *weakHandler;
|
|
558
|
+
@autoreleasepool {
|
|
559
|
+
UIGestureRecognizer *handler = [UIGestureRecognizer new];
|
|
560
|
+
weakHandler = handler;
|
|
561
|
+
self.bridge.reactTouchHandler = handler;
|
|
562
|
+
XCTAssertTrue([self.bridge canPreventGestureRecognizer:handler]);
|
|
563
|
+
XCTAssertFalse([self.bridge canPreventGestureRecognizer:self.handler]);
|
|
564
|
+
}
|
|
565
|
+
XCTAssertNil(weakHandler);
|
|
566
|
+
XCTAssertNil(self.bridge.reactTouchHandler);
|
|
567
|
+
[self beginSingleTouch];
|
|
568
|
+
[self.bridge cancelPressForPan:self.pan];
|
|
569
|
+
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@end
|
|
573
|
+
|
|
574
|
+
#pragma mark - Real UIKit delivery through the production pager
|
|
575
|
+
|
|
576
|
+
// Records what RN's Fabric touch handler observes. Its reset dispatches
|
|
577
|
+
// touchCancel for every still-registered touch, which is how a Pressable loses
|
|
578
|
+
// its press when an external recognizer prevents the handler.
|
|
579
|
+
@interface RNCRecordingSurfaceTouchHandler : RCTSurfaceTouchHandler
|
|
580
|
+
@property (nonatomic, strong) NSMutableArray<NSString *> *events;
|
|
581
|
+
@property (nonatomic, assign) NSInteger registeredTouches;
|
|
582
|
+
@end
|
|
583
|
+
|
|
584
|
+
@implementation RNCRecordingSurfaceTouchHandler
|
|
585
|
+
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
586
|
+
{
|
|
587
|
+
[super touchesBegan:touches withEvent:event];
|
|
588
|
+
self.registeredTouches += touches.count;
|
|
589
|
+
[self.events addObject:@"began"];
|
|
590
|
+
}
|
|
591
|
+
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
592
|
+
{
|
|
593
|
+
[super touchesMoved:touches withEvent:event];
|
|
594
|
+
if (![self.events.lastObject isEqualToString:@"moved"]) [self.events addObject:@"moved"];
|
|
595
|
+
}
|
|
596
|
+
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
597
|
+
{
|
|
598
|
+
[super touchesEnded:touches withEvent:event];
|
|
599
|
+
self.registeredTouches -= touches.count;
|
|
600
|
+
[self.events addObject:@"ended"];
|
|
601
|
+
}
|
|
602
|
+
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
603
|
+
{
|
|
604
|
+
[super touchesCancelled:touches withEvent:event];
|
|
605
|
+
self.registeredTouches -= touches.count;
|
|
606
|
+
[self.events addObject:@"cancelled"];
|
|
607
|
+
}
|
|
608
|
+
- (void)reset
|
|
609
|
+
{
|
|
610
|
+
NSInteger pending = self.registeredTouches;
|
|
611
|
+
[super reset];
|
|
612
|
+
self.registeredTouches = 0;
|
|
613
|
+
if (pending > 0) [self.events addObject:@"reset-cancelled"];
|
|
614
|
+
}
|
|
615
|
+
@end
|
|
616
|
+
|
|
617
|
+
@interface RNCCollapsiblePagerPressCancellationDeliveryTests : XCTestCase
|
|
618
|
+
@property (nonatomic, strong) UIWindow *window;
|
|
619
|
+
@property (nonatomic, strong) UIView *surface;
|
|
620
|
+
@property (nonatomic, strong) RNCRecordingSurfaceTouchHandler *handler;
|
|
621
|
+
@property (nonatomic, strong) RNCCollapsiblePagerViewComponentView *pager;
|
|
622
|
+
@property (nonatomic, strong) RNCSynthesizedTouchDriver *driver;
|
|
623
|
+
@property (nonatomic, strong) UIView *firstPage;
|
|
624
|
+
@end
|
|
625
|
+
|
|
626
|
+
@implementation RNCCollapsiblePagerPressCancellationDeliveryTests
|
|
627
|
+
|
|
628
|
+
- (void)setUp
|
|
629
|
+
{
|
|
630
|
+
[super setUp];
|
|
631
|
+
self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 390, 800)];
|
|
632
|
+
self.window.hidden = NO;
|
|
633
|
+
UIView *host = [[UIView alloc] initWithFrame:self.window.bounds];
|
|
634
|
+
self.surface = [[UIView alloc] initWithFrame:host.bounds];
|
|
635
|
+
[self.window addSubview:host];
|
|
636
|
+
[host addSubview:self.surface];
|
|
637
|
+
self.handler = [RNCRecordingSurfaceTouchHandler new];
|
|
638
|
+
self.handler.events = [NSMutableArray new];
|
|
639
|
+
[self.handler attachToView:self.surface];
|
|
640
|
+
|
|
641
|
+
self.pager = [RNCCollapsiblePagerViewComponentView new];
|
|
642
|
+
auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
643
|
+
props->nativeSmoothHeaderScrollEnabled = true;
|
|
644
|
+
props->headerHeight = 120;
|
|
645
|
+
props->stickyHeaderHeight = 44;
|
|
646
|
+
[self.pager updateProps:props oldProps:nullptr];
|
|
647
|
+
self.pager.frame = self.surface.bounds;
|
|
648
|
+
[self.surface addSubview:self.pager];
|
|
649
|
+
NSArray<UIView<RCTComponentViewProtocol> *> *children = @[
|
|
650
|
+
[RCTViewComponentView new], // header
|
|
651
|
+
[RCTViewComponentView new], // sticky header
|
|
652
|
+
[RCTViewComponentView new], // page 0
|
|
653
|
+
[RCTViewComponentView new], // page 1
|
|
654
|
+
];
|
|
655
|
+
children[0].frame = CGRectMake(0, 0, 390, 120);
|
|
656
|
+
children[1].frame = CGRectMake(0, 0, 390, 44);
|
|
657
|
+
[children enumerateObjectsUsingBlock:^(UIView<RCTComponentViewProtocol> *child,
|
|
658
|
+
NSUInteger index,
|
|
659
|
+
BOOL *stop) {
|
|
660
|
+
[self.pager mountChildComponentView:child index:(NSInteger)index];
|
|
661
|
+
}];
|
|
662
|
+
self.firstPage = children[2];
|
|
663
|
+
[self.window layoutIfNeeded];
|
|
664
|
+
[self drainMainQueue];
|
|
665
|
+
self.firstPage.frame = self.firstPage.superview.bounds;
|
|
666
|
+
self.driver = [[RNCSynthesizedTouchDriver alloc] initWithWindow:self.window];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
- (void)tearDown
|
|
670
|
+
{
|
|
671
|
+
[self.pager removeFromSuperview];
|
|
672
|
+
[self.handler detachFromView:self.surface];
|
|
673
|
+
self.window.hidden = YES;
|
|
674
|
+
self.driver = nil;
|
|
675
|
+
self.firstPage = nil;
|
|
676
|
+
self.pager = nil;
|
|
677
|
+
self.handler = nil;
|
|
678
|
+
self.surface = nil;
|
|
679
|
+
self.window = nil;
|
|
680
|
+
[super tearDown];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
- (void)drainMainQueue
|
|
684
|
+
{
|
|
685
|
+
XCTestExpectation *drained = [self expectationWithDescription:@"main queue drained"];
|
|
686
|
+
dispatch_async(dispatch_get_main_queue(), ^{ [drained fulfill]; });
|
|
687
|
+
[self waitForExpectations:@[drained] timeout:2];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
- (void)assertFixtureIsReadyForDelivery
|
|
691
|
+
{
|
|
692
|
+
XCTAssertNil(self.driver.unavailableReason);
|
|
693
|
+
UIGestureRecognizer *bridge = [self.pager valueForKey:@"contentPressCancellationGesture"];
|
|
694
|
+
UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
|
|
695
|
+
XCTAssertNotNil(bridge);
|
|
696
|
+
XCTAssertEqual([bridge valueForKey:@"reactTouchHandler"], self.handler);
|
|
697
|
+
XCTAssertEqual((id)pagerScrollView.delegate, self.pager);
|
|
698
|
+
XCTAssertGreaterThan(pagerScrollView.contentSize.width, CGRectGetWidth(pagerScrollView.bounds));
|
|
699
|
+
// The touch must start on React page content, outside the shared header.
|
|
700
|
+
CGPoint point = [self pressPoint];
|
|
701
|
+
UIView *hit = [self.window hitTest:point withEvent:nil];
|
|
702
|
+
XCTAssertTrue(hit == self.firstPage || [hit isDescendantOfView:self.firstPage],
|
|
703
|
+
@"hit view %@", hit);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
- (CGPoint)pressPoint
|
|
707
|
+
{
|
|
708
|
+
return CGPointMake(200, 600);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
- (void)testHorizontalPagerDragCancelsReactPressBeforeFingerLifts
|
|
712
|
+
{
|
|
713
|
+
[self assertFixtureIsReadyForDelivery];
|
|
714
|
+
UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
|
|
715
|
+
CGPoint point = [self pressPoint];
|
|
716
|
+
UITouch *touch = [self.driver beginAt:point];
|
|
717
|
+
XCTAssertEqualObjects(self.handler.events, @[@"began"]);
|
|
718
|
+
XCTAssertEqual(self.handler.registeredTouches, 1);
|
|
719
|
+
|
|
720
|
+
for (NSInteger step = 1; step <= 8 && !pagerScrollView.isDragging; ++step) {
|
|
721
|
+
[self.driver move:touch to:CGPointMake(point.x - 12 * step, point.y)];
|
|
722
|
+
}
|
|
723
|
+
XCTAssertTrue(pagerScrollView.isDragging, @"the real pager pan must begin");
|
|
724
|
+
// Finger is still down: RN must already have cancelled its responder.
|
|
725
|
+
XCTAssertEqual(self.handler.registeredTouches, 0);
|
|
726
|
+
XCTAssertTrue([self.handler.events containsObject:@"reset-cancelled"] ||
|
|
727
|
+
[self.handler.events containsObject:@"cancelled"],
|
|
728
|
+
@"handler events %@", self.handler.events);
|
|
729
|
+
XCTAssertFalse([self.handler.events containsObject:@"ended"]);
|
|
730
|
+
NSUInteger eventsBeforeLift = self.handler.events.count;
|
|
731
|
+
|
|
732
|
+
[self.driver end:touch];
|
|
733
|
+
// UIKit must not deliver the lift to the prevented handler as a press end.
|
|
734
|
+
XCTAssertFalse([self.handler.events containsObject:@"ended"], @"events %@", self.handler.events);
|
|
735
|
+
XCTAssertEqual(self.handler.events.count, eventsBeforeLift);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
- (void)testStationaryTapKeepsReactPress
|
|
739
|
+
{
|
|
740
|
+
[self assertFixtureIsReadyForDelivery];
|
|
741
|
+
UITouch *touch = [self.driver beginAt:[self pressPoint]];
|
|
742
|
+
[self.driver end:touch];
|
|
743
|
+
XCTAssertEqualObjects(self.handler.events, (@[@"began", @"ended"]));
|
|
744
|
+
XCTAssertEqual(self.handler.registeredTouches, 0);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
- (void)testSmallMovementBelowPanThresholdKeepsReactPress
|
|
748
|
+
{
|
|
749
|
+
[self assertFixtureIsReadyForDelivery];
|
|
750
|
+
UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
|
|
751
|
+
CGPoint point = [self pressPoint];
|
|
752
|
+
UITouch *touch = [self.driver beginAt:point];
|
|
753
|
+
[self.driver move:touch to:CGPointMake(point.x - 2, point.y)];
|
|
754
|
+
XCTAssertFalse(pagerScrollView.isDragging);
|
|
755
|
+
[self.driver end:touch];
|
|
756
|
+
XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
@end
|