@onekeyfe/react-native-pager-view 3.0.157-alpha.258 → 3.0.157-alpha.273
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 +4 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +69 -27
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +37 -10
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +16 -1
- package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
- package/docs/SPEC.md +59 -4
- package/ios/RNCCollapsiblePagerViewComponentView.mm +52 -2
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +470 -4
- package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +69 -0
- package/lib/module/CollapsiblePagerView.js +10 -1
- package/lib/typescript/src/CollapsiblePagerView.d.ts +1 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerView.tsx +11 -1
|
@@ -1,6 +1,202 @@
|
|
|
1
1
|
#import <XCTest/XCTest.h>
|
|
2
2
|
#import <UIKit/UIGestureRecognizerSubclass.h>
|
|
3
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
|
|
4
200
|
|
|
5
201
|
// Private declarations access the production class compiled into the pod.
|
|
6
202
|
@interface RNCCollapsiblePagerContentPressCancellationGestureRecognizer : UIGestureRecognizer
|
|
@@ -9,8 +205,8 @@
|
|
|
9
205
|
- (void)observeIncomingTouch:(UITouch *)touch;
|
|
10
206
|
@end
|
|
11
207
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
208
|
+
// Terminal states are reset by UIKit once it processes them. Observe requests
|
|
209
|
+
// while retaining the production implementation and UIKit setter.
|
|
14
210
|
@interface RNCPressCancellationObservedBridge : RNCCollapsiblePagerContentPressCancellationGestureRecognizer
|
|
15
211
|
@property (nonatomic, strong) NSMutableArray<NSNumber *> *requestedStates;
|
|
16
212
|
@end
|
|
@@ -23,8 +219,9 @@
|
|
|
23
219
|
}
|
|
24
220
|
@end
|
|
25
221
|
|
|
26
|
-
// These doubles supply input facts
|
|
27
|
-
//
|
|
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.
|
|
28
225
|
@interface RNCPressCancellationTestTouch : UITouch
|
|
29
226
|
@property (nonatomic, strong) UIView *testView;
|
|
30
227
|
@property (nonatomic, assign) UITouchPhase testPhase;
|
|
@@ -49,6 +246,7 @@
|
|
|
49
246
|
@end
|
|
50
247
|
|
|
51
248
|
@interface RNCCollapsiblePagerPressCancellationTests : XCTestCase
|
|
249
|
+
@property (nonatomic, strong) UIWindow *window;
|
|
52
250
|
@property (nonatomic, strong) UIView *host;
|
|
53
251
|
@property (nonatomic, strong) UIView *surface;
|
|
54
252
|
@property (nonatomic, strong) UIView *content;
|
|
@@ -63,9 +261,16 @@
|
|
|
63
261
|
- (void)setUp
|
|
64
262
|
{
|
|
65
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;
|
|
66
270
|
self.host = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
67
271
|
self.surface = [[UIView alloc] initWithFrame:self.host.bounds];
|
|
68
272
|
self.content = [[UIView alloc] initWithFrame:self.surface.bounds];
|
|
273
|
+
[self.window addSubview:self.host];
|
|
69
274
|
[self.host addSubview:self.surface];
|
|
70
275
|
[self.surface addSubview:self.content];
|
|
71
276
|
self.handler = [RCTSurfaceTouchHandler new];
|
|
@@ -75,6 +280,8 @@
|
|
|
75
280
|
self.bridge = [RNCPressCancellationObservedBridge new];
|
|
76
281
|
self.bridge.reactTouchHandler = self.handler;
|
|
77
282
|
[self.host addGestureRecognizer:self.bridge];
|
|
283
|
+
XCTAssertEqual(self.bridge.view.window, self.window);
|
|
284
|
+
XCTAssertEqual(self.handler.view.window, self.window);
|
|
78
285
|
self.pan = [RNCPressCancellationTestPan new];
|
|
79
286
|
self.pan.testState = UIGestureRecognizerStateBegan;
|
|
80
287
|
self.touch = [self touchInView:self.content phase:UITouchPhaseBegan];
|
|
@@ -90,7 +297,10 @@
|
|
|
90
297
|
self.pan = nil;
|
|
91
298
|
self.content = nil;
|
|
92
299
|
self.surface = nil;
|
|
300
|
+
[self.host removeFromSuperview];
|
|
93
301
|
self.host = nil;
|
|
302
|
+
self.window.hidden = YES;
|
|
303
|
+
self.window = nil;
|
|
94
304
|
[super tearDown];
|
|
95
305
|
}
|
|
96
306
|
|
|
@@ -143,6 +353,8 @@
|
|
|
143
353
|
}
|
|
144
354
|
self.pan.testState = UIGestureRecognizerStateBegan;
|
|
145
355
|
[self.bridge cancelPressForPan:self.pan];
|
|
356
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
357
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
146
358
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
147
359
|
}
|
|
148
360
|
|
|
@@ -178,6 +390,8 @@
|
|
|
178
390
|
[self.bridge touchesBegan:[NSSet setWithObject:self.touch]
|
|
179
391
|
withEvent:[self eventWithTouches:[NSSet setWithObjects:self.touch, other, ended, cancelled, nil]]];
|
|
180
392
|
[self.bridge cancelPressForPan:self.pan];
|
|
393
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
394
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
181
395
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
182
396
|
}
|
|
183
397
|
|
|
@@ -196,6 +410,8 @@
|
|
|
196
410
|
{
|
|
197
411
|
[self beginSingleTouch];
|
|
198
412
|
[self.bridge cancelPressForPan:self.pan];
|
|
413
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
414
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
199
415
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
200
416
|
RNCPressCancellationTestTouch *second = [self touchInView:self.content phase:UITouchPhaseBegan];
|
|
201
417
|
RNCPressCancellationTestEvent *event =
|
|
@@ -247,6 +463,8 @@
|
|
|
247
463
|
[self.bridge reset];
|
|
248
464
|
[self beginSingleTouch];
|
|
249
465
|
[self.bridge cancelPressForPan:self.pan];
|
|
466
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
467
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
250
468
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
251
469
|
}
|
|
252
470
|
|
|
@@ -258,6 +476,8 @@
|
|
|
258
476
|
[self beginSingleTouch];
|
|
259
477
|
[self.bridge observeIncomingTouch:second];
|
|
260
478
|
[self.bridge cancelPressForPan:self.pan];
|
|
479
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
480
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
261
481
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
262
482
|
}
|
|
263
483
|
|
|
@@ -279,6 +499,8 @@
|
|
|
279
499
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
|
|
280
500
|
[self beginSingleTouch];
|
|
281
501
|
[self.bridge cancelPressForPan:self.pan];
|
|
502
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
503
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
282
504
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
283
505
|
}
|
|
284
506
|
|
|
@@ -290,6 +512,8 @@
|
|
|
290
512
|
weakEvent = event;
|
|
291
513
|
[self.bridge touchesBegan:[NSSet setWithObject:self.touch] withEvent:event];
|
|
292
514
|
[self.bridge cancelPressForPan:self.pan];
|
|
515
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
516
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
293
517
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
294
518
|
self.touch.testPhase = UITouchPhaseEnded;
|
|
295
519
|
[self.bridge touchesEnded:[NSSet setWithObject:self.touch] withEvent:event];
|
|
@@ -318,6 +542,8 @@
|
|
|
318
542
|
{
|
|
319
543
|
[self beginSingleTouch];
|
|
320
544
|
[self.bridge cancelPressForPan:self.pan];
|
|
545
|
+
XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
|
|
546
|
+
@"The production bridge must request recognition before UIKit retains Began");
|
|
321
547
|
XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
|
|
322
548
|
self.touch.testPhase = UITouchPhaseCancelled;
|
|
323
549
|
[self.bridge touchesCancelled:[NSSet setWithObject:self.touch]
|
|
@@ -344,3 +570,243 @@
|
|
|
344
570
|
}
|
|
345
571
|
|
|
346
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)testVerticalPageDragCancelsReactPressBeforeFingerLifts
|
|
748
|
+
{
|
|
749
|
+
[self assertFixtureIsReadyForDelivery];
|
|
750
|
+
CGPoint point = [self pressPoint];
|
|
751
|
+
UITouch *touch = [self.driver beginAt:point];
|
|
752
|
+
XCTAssertEqual(self.handler.registeredTouches, 1);
|
|
753
|
+
|
|
754
|
+
[self.driver move:touch to:CGPointMake(point.x, point.y - 12)];
|
|
755
|
+
XCTAssertEqual(self.handler.registeredTouches, 0);
|
|
756
|
+
XCTAssertTrue([self.handler.events containsObject:@"reset-cancelled"] ||
|
|
757
|
+
[self.handler.events containsObject:@"cancelled"],
|
|
758
|
+
@"handler events %@", self.handler.events);
|
|
759
|
+
XCTAssertFalse([self.handler.events containsObject:@"ended"]);
|
|
760
|
+
|
|
761
|
+
[self.driver end:touch];
|
|
762
|
+
XCTAssertFalse([self.handler.events containsObject:@"ended"], @"events %@", self.handler.events);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
- (void)testVerticalSiblingControlDragKeepsReactPress
|
|
766
|
+
{
|
|
767
|
+
[self assertFixtureIsReadyForDelivery];
|
|
768
|
+
UIView *sibling = [RCTViewComponentView new];
|
|
769
|
+
sibling.frame = CGRectMake(160, 540, 80, 100);
|
|
770
|
+
[self.surface addSubview:sibling];
|
|
771
|
+
CGPoint point = [self pressPoint];
|
|
772
|
+
UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
|
|
773
|
+
XCTAssertEqual(sibling.superview, self.pager.superview);
|
|
774
|
+
XCTAssertFalse([sibling isDescendantOfView:pagerScrollView]);
|
|
775
|
+
XCTAssertEqual([self.window hitTest:point withEvent:nil], sibling);
|
|
776
|
+
|
|
777
|
+
UITouch *touch = [self.driver beginAt:point];
|
|
778
|
+
XCTAssertEqual(self.handler.registeredTouches, 1);
|
|
779
|
+
[self.driver move:touch to:CGPointMake(point.x, point.y - 12)];
|
|
780
|
+
XCTAssertEqual(self.handler.registeredTouches, 1);
|
|
781
|
+
XCTAssertFalse([self.handler.events containsObject:@"reset-cancelled"]);
|
|
782
|
+
XCTAssertFalse([self.handler.events containsObject:@"cancelled"]);
|
|
783
|
+
|
|
784
|
+
[self.driver end:touch];
|
|
785
|
+
XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
|
|
786
|
+
XCTAssertEqual(self.handler.registeredTouches, 0);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
- (void)testSmallVerticalMovementKeepsReactPress
|
|
790
|
+
{
|
|
791
|
+
[self assertFixtureIsReadyForDelivery];
|
|
792
|
+
CGPoint point = [self pressPoint];
|
|
793
|
+
UITouch *touch = [self.driver beginAt:point];
|
|
794
|
+
[self.driver move:touch to:CGPointMake(point.x, point.y - 8)];
|
|
795
|
+
XCTAssertEqual(self.handler.registeredTouches, 1);
|
|
796
|
+
[self.driver end:touch];
|
|
797
|
+
XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
- (void)testSmallMovementBelowPanThresholdKeepsReactPress
|
|
801
|
+
{
|
|
802
|
+
[self assertFixtureIsReadyForDelivery];
|
|
803
|
+
UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
|
|
804
|
+
CGPoint point = [self pressPoint];
|
|
805
|
+
UITouch *touch = [self.driver beginAt:point];
|
|
806
|
+
[self.driver move:touch to:CGPointMake(point.x - 2, point.y)];
|
|
807
|
+
XCTAssertFalse(pagerScrollView.isDragging);
|
|
808
|
+
[self.driver end:touch];
|
|
809
|
+
XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
@end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#import <XCTest/XCTest.h>
|
|
2
|
+
#import <React/RCTViewComponentView.h>
|
|
2
3
|
#import "RNCCollapsiblePagerViewComponentView.h"
|
|
3
4
|
|
|
4
5
|
@interface RNCCollapsiblePagerViewComponentView (RefreshControlTesting)
|
|
5
6
|
- (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
6
7
|
- (void)releaseRefreshControlForScrollView:(UIScrollView *)scrollView;
|
|
8
|
+
- (void)attachScrollObserverForCurrentPage;
|
|
7
9
|
@end
|
|
8
10
|
|
|
9
11
|
@interface RNCCollapsiblePagerRefreshControlTests : XCTestCase
|
|
@@ -177,4 +179,71 @@
|
|
|
177
179
|
XCTAssertTrue(control.layer.masksToBounds);
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
- (void)testRefreshTopInsetDoesNotDisplaceAttachedSharedHeader
|
|
183
|
+
{
|
|
184
|
+
RNCCollapsiblePagerViewComponentView *pager = [self pagerWithHeader:64];
|
|
185
|
+
[pager mountChildComponentView:[RCTViewComponentView new] index:0];
|
|
186
|
+
[pager mountChildComponentView:[RCTViewComponentView new] index:1];
|
|
187
|
+
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
|
|
188
|
+
scroll.refreshControl = [UIRefreshControl new];
|
|
189
|
+
scroll.contentSize = CGSizeMake(320, 2000);
|
|
190
|
+
scroll.contentInset = UIEdgeInsetsMake(232, 0, 0, 0);
|
|
191
|
+
UIView *content = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 2000)];
|
|
192
|
+
[scroll addSubview:content];
|
|
193
|
+
UIViewController *page = [UIViewController new];
|
|
194
|
+
[page.view addSubview:scroll];
|
|
195
|
+
NSMutableArray<UIViewController *> *pages = [pager valueForKey:@"pageControllers"];
|
|
196
|
+
[pages addObject:page];
|
|
197
|
+
[pager attachScrollObserverForCurrentPage];
|
|
198
|
+
UIView *headerHost = [pager valueForKey:@"sharedHeaderHostView"];
|
|
199
|
+
UIView *stickyHeader = [pager valueForKey:@"stickyHeaderView"];
|
|
200
|
+
UIView *tabBar = [pager valueForKey:@"nativeTabBarView"];
|
|
201
|
+
XCTAssertNotNil(stickyHeader);
|
|
202
|
+
XCTAssertEqual(stickyHeader.superview, headerHost);
|
|
203
|
+
XCTAssertEqual(headerHost.superview, scroll);
|
|
204
|
+
XCTAssertEqualWithAccuracy(scroll.contentInset.top, 340, 0.001);
|
|
205
|
+
XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
|
|
206
|
+
|
|
207
|
+
scroll.contentOffset = CGPointMake(0, 500);
|
|
208
|
+
CGFloat baselineStickyTranslation = stickyHeader.transform.ty;
|
|
209
|
+
CGFloat baselineTabBarTranslation = tabBar.transform.ty;
|
|
210
|
+
XCTAssertEqualWithAccuracy(baselineStickyTranslation, 776, 0.001);
|
|
211
|
+
XCTAssertEqualWithAccuracy(baselineTabBarTranslation, 776, 0.001);
|
|
212
|
+
CGPoint offset = scroll.contentOffset;
|
|
213
|
+
scroll.contentInset = UIEdgeInsetsMake(400, 0, 0, 0);
|
|
214
|
+
XCTAssertTrue(CGPointEqualToPoint(scroll.contentOffset, offset));
|
|
215
|
+
XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
|
|
216
|
+
XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, baselineStickyTranslation, 0.001);
|
|
217
|
+
XCTAssertEqualWithAccuracy(tabBar.transform.ty, baselineTabBarTranslation, 0.001);
|
|
218
|
+
XCTAssertEqualWithAccuracy(CGRectGetMaxY(headerHost.frame), CGRectGetMinY(content.frame), 0.001);
|
|
219
|
+
|
|
220
|
+
// Exercise actual scroll notifications while the temporary refresh inset is
|
|
221
|
+
// present, including direction reversals and the sticky/unpinned boundary.
|
|
222
|
+
// The pre-fix formula adds the extra 60 points on these scroll updates.
|
|
223
|
+
for (NSNumber *nextOffset in @[@620, @450, @-300, @-340, @500]) {
|
|
224
|
+
scroll.contentOffset = CGPointMake(0, nextOffset.doubleValue);
|
|
225
|
+
CGFloat expectedTranslation = MAX(0, nextOffset.doubleValue + 340 - 64);
|
|
226
|
+
XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
|
|
227
|
+
XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, expectedTranslation, 0.001);
|
|
228
|
+
XCTAssertEqualWithAccuracy(tabBar.transform.ty, expectedTranslation, 0.001);
|
|
229
|
+
XCTAssertEqualWithAccuracy(CGRectGetMaxY(headerHost.frame), CGRectGetMinY(content.frame), 0.001);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
offset = scroll.contentOffset;
|
|
233
|
+
scroll.contentInset = UIEdgeInsetsMake(340, 0, 0, 0);
|
|
234
|
+
XCTAssertTrue(CGPointEqualToPoint(scroll.contentOffset, offset));
|
|
235
|
+
XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
|
|
236
|
+
XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, baselineStickyTranslation, 0.001);
|
|
237
|
+
XCTAssertEqualWithAccuracy(tabBar.transform.ty, baselineTabBarTranslation, 0.001);
|
|
238
|
+
|
|
239
|
+
scroll.contentInset = UIEdgeInsetsMake(400, 0, 0, 0);
|
|
240
|
+
XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
|
|
241
|
+
XCTAssertEqualWithAccuracy(stickyHeader.transform.ty, baselineStickyTranslation, 0.001);
|
|
242
|
+
XCTAssertEqualWithAccuracy(tabBar.transform.ty, baselineTabBarTranslation, 0.001);
|
|
243
|
+
XCTAssertEqualWithAccuracy(CGRectGetMaxY(headerHost.frame), CGRectGetMinY(content.frame), 0.001);
|
|
244
|
+
scroll.refreshControl = nil;
|
|
245
|
+
XCTAssertEqualWithAccuracy(headerHost.transform.ty, 0, 0.001);
|
|
246
|
+
[pager prepareForRecycle];
|
|
247
|
+
}
|
|
248
|
+
|
|
180
249
|
@end
|