@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.
@@ -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
- // UIKit can reset terminal states immediately outside real touch delivery.
13
- // Observe requests while retaining the production implementation and UIKit setter.
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 only. The production bridge owns its state
27
- // transitions; this suite does not claim to synthesize UIKit touch delivery.
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