@onekeyfe/react-native-pager-view 6.7.0-2 → 6.7.0-21

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.
Files changed (43) hide show
  1. package/README.md +14 -0
  2. package/android/src/androidTest/AndroidManifest.xml +7 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +306 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +130 -30
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +27 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +19 -0
  8. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +50 -6
  9. package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +8 -2
  10. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +40 -0
  11. package/docs/SPEC.md +257 -10
  12. package/ios/RNCCollapsiblePagerViewComponentView.mm +614 -42
  13. package/ios/RNCNativeScrollerComponentView.h +2 -0
  14. package/ios/RNCNativeScrollerComponentView.mm +42 -0
  15. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +403 -1
  16. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +338 -0
  17. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +74 -9
  18. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +50 -0
  19. package/lib/module/CollapsiblePagerView.js +32 -7
  20. package/lib/module/CollapsiblePagerView.web.js +439 -39
  21. package/lib/module/CollapsiblePagerViewNativeComponent.ts +13 -0
  22. package/lib/module/NativeScroller.web.js +16 -3
  23. package/lib/module/WebCollapsiblePagerPage.js +182 -0
  24. package/lib/module/index.js +2 -0
  25. package/lib/module/index.native.js +9 -0
  26. package/lib/module/index.web.js +2 -0
  27. package/lib/typescript/src/CollapsiblePagerView.d.ts +10 -3
  28. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +66 -3
  29. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +5 -0
  30. package/lib/typescript/src/WebCollapsiblePagerPage.d.ts +42 -0
  31. package/lib/typescript/src/index.d.ts +4 -0
  32. package/lib/typescript/src/index.native.d.ts +13 -0
  33. package/lib/typescript/src/index.web.d.ts +4 -0
  34. package/package.json +6 -4
  35. package/react-native-pager-view.podspec +1 -0
  36. package/src/CollapsiblePagerView.tsx +99 -8
  37. package/src/CollapsiblePagerView.web.tsx +657 -51
  38. package/src/CollapsiblePagerViewNativeComponent.ts +13 -0
  39. package/src/NativeScroller.web.tsx +24 -2
  40. package/src/WebCollapsiblePagerPage.tsx +258 -0
  41. package/src/index.native.tsx +30 -0
  42. package/src/index.tsx +4 -0
  43. package/src/index.web.tsx +4 -0
@@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN
13
13
  - (void)nativeScrollerWillReceiveScrollCommand:(RNCNativeScrollerComponentView *)scroller;
14
14
  - (void)nativeScroller:(RNCNativeScrollerComponentView *)scroller
15
15
  callerInsetChangedFrom:(UIEdgeInsets)previous to:(UIEdgeInsets)next;
16
+ @optional
17
+ - (void)nativeScrollViewDidBecomeReady:(UIScrollView *)scrollView;
16
18
  @end
17
19
 
18
20
  NS_ASSUME_NONNULL_END
@@ -8,6 +8,25 @@
8
8
 
9
9
  using namespace facebook::react;
10
10
 
11
+ static void RNCNativeScrollerHomeVisualLog(NSString *phase, UIScrollView *scrollView)
12
+ {
13
+ #if DEBUG
14
+ Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog");
15
+ if (logClass == nil) logClass = NSClassFromString(@"OneKeyLog");
16
+ SEL selector = NSSelectorFromString(@"debug::");
17
+ if (logClass == nil || ![logClass respondsToSelector:selector]) return;
18
+ UIRefreshControl *control = scrollView.refreshControl;
19
+ NSString *message = [NSString stringWithFormat:
20
+ @"[IOS-HOME-VISUAL] epochMs=%.0f monoMs=%.0f phase=scroller-%@ scrollTag=%ld refresh=%d refreshing=%d scrollY=%.1f insetTop=%.1f",
21
+ [NSDate.date timeIntervalSince1970] * 1000, CACurrentMediaTime() * 1000,
22
+ phase, (long)scrollView.tag, control != nil, control.isRefreshing,
23
+ scrollView.contentOffset.y, scrollView.contentInset.top];
24
+ typedef void (*LogFunction)(id, SEL, NSString *, NSString *);
25
+ LogFunction logFunction = (LogFunction)[logClass methodForSelector:selector];
26
+ logFunction(logClass, selector, @"NativeScroller", message);
27
+ #endif
28
+ }
29
+
11
30
  @interface RNCNativeVerticalScrollView : UIScrollView
12
31
  @end
13
32
  @implementation RNCNativeVerticalScrollView
@@ -156,6 +175,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
156
175
  [_refreshControl addTarget:self action:@selector(refreshTriggered:)
157
176
  forControlEvents:UIControlEventValueChanged];
158
177
  _scrollView.refreshControl = _refreshControl;
178
+ RNCNativeScrollerHomeVisualLog(@"refresh-created", _scrollView);
159
179
  } else if (!next.refreshEnabled && _refreshControl != nil) {
160
180
  [_refreshControl endRefreshing];
161
181
  _scrollView.refreshControl = nil;
@@ -174,8 +194,10 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
174
194
  }
175
195
  if (next.refreshing && !_refreshControl.refreshing) {
176
196
  [_refreshControl beginRefreshing];
197
+ RNCNativeScrollerHomeVisualLog(@"refresh-begin-prop", _scrollView);
177
198
  } else if (!next.refreshing && (previous.refreshing || createdRefreshControl)) {
178
199
  [_refreshControl endRefreshing];
200
+ if (previous.refreshing) RNCNativeScrollerHomeVisualLog(@"refresh-end-prop", _scrollView);
179
201
  }
180
202
  }
181
203
 
@@ -276,10 +298,23 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
276
298
  }
277
299
  }
278
300
 
301
+ - (void)notifyCoordinatorScrollViewReadyIfNeeded
302
+ {
303
+ if (_recycled || self.window == nil ||
304
+ CGRectGetWidth(_scrollView.bounds) <= 0 || CGRectGetHeight(_scrollView.bounds) <= 0) return;
305
+ for (UIView *owner = self.superview; owner != nil; owner = owner.superview) {
306
+ if ([owner conformsToProtocol:@protocol(RNCNativeScrollerInsetCoordinator)] &&
307
+ [owner respondsToSelector:@selector(nativeScrollViewDidBecomeReady:)]) {
308
+ [(id<RNCNativeScrollerInsetCoordinator>)owner nativeScrollViewDidBecomeReady:_scrollView];
309
+ }
310
+ }
311
+ }
312
+
279
313
  - (void)didMoveToSuperview
280
314
  {
281
315
  [super didMoveToSuperview];
282
316
  [self updateTopScrollEdge];
317
+ [self notifyCoordinatorScrollViewReadyIfNeeded];
283
318
  }
284
319
 
285
320
  - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
@@ -292,6 +327,10 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
292
327
  if (!CGSizeEqualToSize(previous, _scrollView.bounds.size)) {
293
328
  _needsContentClamp = YES;
294
329
  [self scheduleContentClamp];
330
+ if ((previous.width <= 0 || previous.height <= 0) &&
331
+ _scrollView.bounds.size.width > 0 && _scrollView.bounds.size.height > 0) {
332
+ [self notifyCoordinatorScrollViewReadyIfNeeded];
333
+ }
295
334
  }
296
335
  }
297
336
 
@@ -379,6 +418,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
379
418
  const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
380
419
  if (!props.refreshEnabled || props.refreshing) return;
381
420
  _refreshEventSentForDrag = YES;
421
+ RNCNativeScrollerHomeVisualLog(@"refresh-native-trigger", _scrollView);
382
422
  if (_eventEmitter) {
383
423
  std::static_pointer_cast<const RNCNativeScrollerEventEmitter>(_eventEmitter)->onRefresh({});
384
424
  }
@@ -523,6 +563,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
523
563
  - (void)setRefreshing:(BOOL)refreshing
524
564
  {
525
565
  if (_recycled || _refreshControl == nil) return;
566
+ RNCNativeScrollerHomeVisualLog(refreshing ? @"refresh-command-start" : @"refresh-command-stop", _scrollView);
526
567
  if (refreshing) {
527
568
  if (!_refreshControl.refreshing) [_refreshControl beginRefreshing];
528
569
  } else {
@@ -550,6 +591,7 @@ static Event RNCScrollerMetrics(UIScrollView *scroll, CGPoint velocity, CGPoint
550
591
  const auto &props = *std::static_pointer_cast<const RNCNativeScrollerProps>(_props);
551
592
  if (props.refreshing) [_refreshControl beginRefreshing];
552
593
  [self scheduleContentClamp];
594
+ [self notifyCoordinatorScrollViewReadyIfNeeded];
553
595
  }
554
596
  }
555
597
 
@@ -1,6 +1,8 @@
1
1
  #import <XCTest/XCTest.h>
2
2
  #import "RNCCollapsiblePagerViewComponentView.h"
3
3
  #import "RNCNativeScrollerComponentView.h"
4
+ #import <React/RCTScrollViewComponentView.h>
5
+ #import <react/renderer/components/pagerview/Props.h>
4
6
  #import <react/renderer/mounting/MountingTransaction.h>
5
7
 
6
8
  using namespace facebook::react;
@@ -12,6 +14,7 @@ using namespace facebook::react;
12
14
  withSurfaceTelemetry:(const SurfaceTelemetry &)telemetry;
13
15
  - (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view;
14
16
  - (UIScrollView *)findVerticalScrollViewInView:(UIView *)view;
17
+ - (UIScrollView *)verticalScrollViewForPageAtIndex:(NSInteger)pageIndex;
15
18
  - (void)attachScrollObserverForCurrentPage;
16
19
  - (void)attachScrollObserverForCurrentPageAfterTransition:(BOOL)selectionSettled;
17
20
  - (void)detachScrollObserver;
@@ -22,6 +25,10 @@ using namespace facebook::react;
22
25
  - (void)updateHeaderSettledStateForceBusy:(BOOL)forceBusy;
23
26
  - (void)preparePageForHorizontalTransitionAtIndex:(NSInteger)index;
24
27
  - (void)applyInsetsToScrollView:(UIScrollView *)scrollView pageIndex:(NSInteger)index restore:(BOOL)restore;
28
+ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps;
29
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index;
30
+ - (CGFloat)effectiveTopOverlayHeight;
31
+ - (void)updateRefreshControlForScrollView:(UIScrollView *)scrollView;
25
32
  @end
26
33
 
27
34
  @interface RNCNativeScrollerComponentView (ScrollCommandTesting)
@@ -34,6 +41,127 @@ using namespace facebook::react;
34
41
 
35
42
  @implementation RNCCollapsiblePagerFirstFrameTests
36
43
 
44
+ - (void)testPendingLocalOverlayReservesManagedScrollerInsetBeforeSlotRebuild
45
+ {
46
+ RNCCollapsiblePagerViewComponentView *pager =
47
+ [[RNCCollapsiblePagerViewComponentView alloc] initWithFrame:CGRectMake(0, 0, 320, 844)];
48
+ UIWindow *window = [[UIWindow alloc] initWithFrame:pager.frame];
49
+ UIViewController *root = [UIViewController new];
50
+ window.rootViewController = root;
51
+ [root.view addSubview:pager];
52
+ window.hidden = NO;
53
+ [self addTeardownBlock:^{
54
+ [pager prepareForRecycle];
55
+ [pager removeFromSuperview];
56
+ window.rootViewController = nil;
57
+ window.hidden = YES;
58
+ }];
59
+
60
+ auto baseline = std::make_shared<RNCCollapsiblePagerViewProps>();
61
+ baseline->nativeSmoothHeaderScrollEnabled = true;
62
+ baseline->headerHeight = 120;
63
+ baseline->stickyHeaderHeight = 44;
64
+ baseline->pageKeys = "[\"page-0\"]";
65
+ [pager updateProps:baseline oldProps:nullptr];
66
+
67
+ UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 120)];
68
+ UIView *sticky = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
69
+ UIView *page = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 844)];
70
+ RNCNativeScrollerComponentView *scroller = [RNCNativeScrollerComponentView new];
71
+ scroller.frame = page.bounds;
72
+ scroller.scrollView.frame = scroller.bounds;
73
+ scroller.scrollView.contentSize = CGSizeMake(320, 1500);
74
+ [page addSubview:scroller];
75
+ [pager mountChildComponentView:(id)header index:0];
76
+ [pager mountChildComponentView:(id)sticky index:1];
77
+ [pager mountChildComponentView:(id)page index:2];
78
+ [root.view layoutIfNeeded];
79
+ [pager layoutIfNeeded];
80
+ XCTestExpectation *settled = [self expectationWithDescription:@"Baseline slots and observer settled"];
81
+ dispatch_async(dispatch_get_main_queue(), ^{ [settled fulfill]; });
82
+ [self waitForExpectations:@[settled] timeout:5];
83
+ [pager attachScrollObserverForCurrentPage];
84
+ UIScrollView *ownedScroll = scroller.scrollView;
85
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
86
+ XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 164, 0.01);
87
+ [scroller scrollTo:0 y:40 - ownedScroll.contentInset.top animated:NO];
88
+ [pager setValue:@40 forKey:@"headerOffset"];
89
+ CGFloat initialLogicalOffset = ownedScroll.contentOffset.y + ownedScroll.contentInset.top;
90
+ XCTAssertEqualWithAccuracy(initialLogicalOffset, 40, 0.01);
91
+
92
+ auto hard = std::make_shared<RNCCollapsiblePagerViewProps>();
93
+ hard->nativeSmoothHeaderScrollEnabled = true;
94
+ hard->headerHeight = 120;
95
+ hard->stickyHeaderHeight = 44;
96
+ hard->pageKeys = "[\"page-0\"]";
97
+ hard->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
98
+ hard->topOverlayHeight = 162;
99
+ [pager updateProps:hard oldProps:baseline];
100
+ XCTAssertNil([pager valueForKey:@"topOverlayView"]);
101
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
102
+ XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 162, 0.01);
103
+ XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
104
+ XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
105
+ initialLogicalOffset, 0.01);
106
+ [self finishMount:pager];
107
+ XCTAssertNil([pager valueForKey:@"topOverlayView"]);
108
+ XCTAssertTrue([[pager valueForKey:@"deferTopEdgeSlotRebuild"] boolValue]);
109
+
110
+ UIView *overlay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 162)];
111
+ [pager mountChildComponentView:(id)overlay index:2];
112
+ XCTAssertNil([pager valueForKey:@"topOverlayView"]);
113
+ [self finishMount:pager];
114
+ UIView *overlayHost = [pager valueForKey:@"topOverlayHostView"];
115
+ XCTAssertEqual([pager valueForKey:@"topOverlayView"], overlay);
116
+ XCTAssertEqual(overlay.superview, overlayHost);
117
+ XCTAssertEqual(overlay.window, window);
118
+ XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
119
+ XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
120
+ initialLogicalOffset, 0.01);
121
+ XCTestExpectation *rebuilt = [self expectationWithDescription:@"Post-mount observer restored"];
122
+ dispatch_async(dispatch_get_main_queue(), ^{ [rebuilt fulfill]; });
123
+ [self waitForExpectations:@[rebuilt] timeout:5];
124
+ [pager attachScrollObserverForCurrentPage];
125
+ XCTAssertEqual([pager valueForKey:@"topOverlayView"], overlay);
126
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], ownedScroll);
127
+ XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 326, 0.01);
128
+ XCTAssertEqualWithAccuracy(ownedScroll.contentOffset.y + ownedScroll.contentInset.top,
129
+ initialLogicalOffset, 0.01);
130
+
131
+ UIRefreshControl *control = [UIRefreshControl new];
132
+ ownedScroll.refreshControl = control;
133
+ [pager updateRefreshControlForScrollView:ownedScroll];
134
+ UIEdgeInsets refreshingInset = ownedScroll.contentInset;
135
+ refreshingInset.top += 60;
136
+ ownedScroll.contentInset = refreshingInset;
137
+ auto disabled = std::make_shared<RNCCollapsiblePagerViewProps>();
138
+ disabled->nativeSmoothHeaderScrollEnabled = true;
139
+ disabled->headerHeight = 120;
140
+ disabled->stickyHeaderHeight = 44;
141
+ disabled->pageKeys = "[\"page-0\"]";
142
+ disabled->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::None;
143
+ disabled->topOverlayHeight = 0;
144
+ [pager updateProps:disabled oldProps:hard];
145
+ XCTAssertEqual(ownedScroll.refreshControl, control);
146
+ XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 0, 0.01);
147
+ XCTAssertEqualWithAccuracy(ownedScroll.contentInset.top, 224, 0.01);
148
+ }
149
+
150
+ - (void)testPendingLocalOverlayHeightDoesNotOverrideExternalGeometry
151
+ {
152
+ RNCCollapsiblePagerViewComponentView *pager = [RNCCollapsiblePagerViewComponentView new];
153
+ auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
154
+ props->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
155
+ props->topOverlayHeight = 162;
156
+ props->externalTopOverlayTag = 999;
157
+ [pager updateProps:props oldProps:nullptr];
158
+ XCTAssertNil([pager valueForKey:@"topOverlayView"]);
159
+ XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 0, 0.01);
160
+ [pager setValue:@64 forKey:@"externalTopOverlayHeight"];
161
+ XCTAssertEqualWithAccuracy([pager effectiveTopOverlayHeight], 64, 0.01);
162
+ [pager prepareForRecycle];
163
+ }
164
+
37
165
  - (void)testHeaderSettledAtTopRequiresCurrentQuietOwnerAndCancelsDetachedTimer
38
166
  {
39
167
  RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
@@ -45,9 +173,10 @@ using namespace facebook::react;
45
173
  RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
46
174
  [pager.window layoutIfNeeded];
47
175
  [pager layoutIfNeeded];
176
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
48
177
  XCTestExpectation *initialLayout = [self expectationWithDescription:@"Initial observer attachment drained"];
49
178
  dispatch_async(dispatch_get_main_queue(), ^{ [initialLayout fulfill]; });
50
- [self waitForExpectations:@[initialLayout] timeout:1];
179
+ [self waitForExpectations:@[initialLayout] timeout:5];
51
180
  [pager detachScrollObserver];
52
181
  [pager attachScrollObserverForCurrentPage];
53
182
  [pager updateHeaderSettledStateForceBusy:NO];
@@ -335,6 +464,138 @@ using namespace facebook::react;
335
464
  XCTAssertTrue([[pager valueForKey:@"pendingPageRestoreKeys"] containsObject:@"page-1"]);
336
465
  }
337
466
 
467
+ - (void)testReleasedOwnerRecoversReplacementWithoutAnotherFabricInsertion
468
+ {
469
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
470
+ // The fixture mounts page 0 directly; UIKit must not select it again.
471
+ [pager setValue:@YES forKey:@"hasAppliedInitialPage"];
472
+ RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
473
+ source.tag = 91001;
474
+ [pager.window layoutIfNeeded];
475
+ [pager layoutIfNeeded];
476
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.15]];
477
+ [pager setValue:@120 forKey:@"headerOffset"];
478
+ [pager attachScrollObserverForCurrentPage];
479
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], source.scrollView);
480
+
481
+ // Finish the old owner's removal before the replacement becomes available.
482
+ ShadowView removedView;
483
+ removedView.tag = (Tag)source.tag;
484
+ MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation(1, removedView, 0)}, {});
485
+ SurfaceTelemetry telemetry;
486
+ [pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
487
+ [source removeFromSuperview];
488
+ [self finishMount:pager];
489
+ XCTAssertNil([pager valueForKey:@"observedScrollView"]);
490
+
491
+ RNCNativeScrollerComponentView *replacement = [self addScrollerToPager:pager atIndex:0];
492
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
493
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], replacement.scrollView);
494
+ XCTAssertEqual([pager valueForKey:@"sharedHeaderScrollView"], replacement.scrollView);
495
+ XCTAssertEqualWithAccuracy(replacement.scrollView.contentOffset.y + replacement.scrollView.contentInset.top,
496
+ 120, 0.01);
497
+ XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], 120, 0.01);
498
+ }
499
+
500
+ - (void)testCurrentPageInsertionRestartsExhaustedReplacementRecovery
501
+ {
502
+ [self assertReplacementRecoveryWithCancelledDrag:NO finalRetryPending:NO];
503
+ }
504
+
505
+ - (void)testCancelledDragRetainsReplacementRecoveryFromInsertion
506
+ {
507
+ [self assertReplacementRecoveryWithCancelledDrag:YES finalRetryPending:NO];
508
+ }
509
+
510
+ - (void)testInsertionInvalidatesPendingFinalReplacementRetry
511
+ {
512
+ [self assertReplacementRecoveryWithCancelledDrag:NO finalRetryPending:YES];
513
+ }
514
+
515
+ - (void)assertReplacementRecoveryWithCancelledDrag:(BOOL)cancelDrag
516
+ finalRetryPending:(BOOL)finalRetryPending
517
+ {
518
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
519
+ [pager setValue:@YES forKey:@"hasAppliedInitialPage"];
520
+ RNCNativeScrollerComponentView *source = [self addScrollerToPager:pager atIndex:0];
521
+ source.tag = 91001;
522
+ UIView *page = source.superview;
523
+ page.tag = 92001;
524
+ UIViewController *otherPage = [pager valueForKey:@"pageControllers"][1];
525
+ otherPage.view.tag = 92002;
526
+ [pager.window layoutIfNeeded];
527
+ [pager layoutIfNeeded];
528
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.15]];
529
+ [pager setValue:@120 forKey:@"headerOffset"];
530
+ [pager attachScrollObserverForCurrentPage];
531
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], source.scrollView);
532
+
533
+ ShadowView removedView;
534
+ removedView.tag = (Tag)source.tag;
535
+ MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation((Tag)page.tag, removedView, 0)}, {});
536
+ SurfaceTelemetry telemetry;
537
+ [pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
538
+ [source removeFromSuperview];
539
+ [self finishMount:pager];
540
+ NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:3];
541
+ while (([[pager valueForKey:@"scrollResolveRetryAttempt"] integerValue] < 12 ||
542
+ (!finalRetryPending && [[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue])) &&
543
+ deadline.timeIntervalSinceNow > 0) {
544
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
545
+ }
546
+ XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryAttempt"], @12);
547
+ XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryScheduled"], @(finalRetryPending));
548
+ XCTAssertNotNil([pager valueForKey:@"releasedPageScrollStates"][@"page-0"]);
549
+
550
+ // Only a new insertion in the current page can reopen the exhausted budget.
551
+ [self finishMount:pager];
552
+ [pager mountingTransactionDidMount:removal withSurfaceTelemetry:telemetry];
553
+ UIView *replacementHost = [UIView new];
554
+ replacementHost.tag = 93001;
555
+ [otherPage.view addSubview:replacementHost];
556
+ ShadowView insertedView;
557
+ insertedView.tag = (Tag)replacementHost.tag;
558
+ MountingTransaction unrelated(1, 2, {ShadowViewMutation::InsertMutation((Tag)otherPage.view.tag, insertedView, 0)}, {});
559
+ [pager mountingTransactionDidMount:unrelated withSurfaceTelemetry:telemetry];
560
+ XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryAttempt"], @12);
561
+ XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryScheduled"], @(finalRetryPending));
562
+
563
+ if (cancelDrag) {
564
+ [pager beginPageStateTransition];
565
+ [pager setValue:@YES forKey:@"transitioning"];
566
+ [pager setValue:@YES forKey:@"isPagerDragging"];
567
+ }
568
+ [page addSubview:replacementHost];
569
+ MountingTransaction insertion(1, 3, {ShadowViewMutation::InsertMutation((Tag)page.tag, insertedView, 0)}, {});
570
+ [pager mountingTransactionDidMount:insertion withSurfaceTelemetry:telemetry];
571
+ XCTAssertNil([pager valueForKey:@"observedScrollView"]);
572
+ NSNumber *retryToken = [pager valueForKey:@"scrollResolveRetryToken"];
573
+ [pager mountingTransactionDidMount:insertion withSurfaceTelemetry:telemetry];
574
+ XCTAssertEqualObjects([pager valueForKey:@"scrollResolveRetryToken"], retryToken);
575
+ if (cancelDrag) {
576
+ XCTAssertNil([pager valueForKey:@"sharedHeaderScrollView"]);
577
+ XCTAssertFalse([[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue]);
578
+ // Settle on the source page while the replacement is still unavailable.
579
+ [pager attachScrollObserverForCurrentPageAfterTransition:YES];
580
+ [pager setValue:@NO forKey:@"isPagerDragging"];
581
+ [pager setValue:@NO forKey:@"transitioning"];
582
+ [pager attachScrollObserverForCurrentPage];
583
+ XCTAssertEqualObjects([pager valueForKey:@"currentIndex"], @0);
584
+ }
585
+ __block RNCNativeScrollerComponentView *replacement;
586
+ int64_t replacementDelay = finalRetryPending ? 150 : 50;
587
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, replacementDelay * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
588
+ replacement = [self addScrollerToPager:pager atIndex:0];
589
+ [replacementHost addSubview:replacement];
590
+ });
591
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
592
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], replacement.scrollView);
593
+ XCTAssertEqual([pager valueForKey:@"sharedHeaderScrollView"], replacement.scrollView);
594
+ XCTAssertEqualWithAccuracy(replacement.scrollView.contentOffset.y + replacement.scrollView.contentInset.top,
595
+ 120, 0.01);
596
+ XCTAssertEqualWithAccuracy([[pager valueForKey:@"headerOffset"] doubleValue], 120, 0.01);
597
+ }
598
+
338
599
  - (void)testRenamedPageDoesNotResurrectPrunedObserverCache
339
600
  {
340
601
  RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
@@ -364,6 +625,8 @@ using namespace facebook::react;
364
625
  window.hidden = NO;
365
626
  [self addTeardownBlock:^{
366
627
  [pager prepareForRecycle];
628
+ [pager removeFromSuperview];
629
+ window.rootViewController = nil;
367
630
  window.hidden = YES;
368
631
  }];
369
632
  NSMutableArray *pages = [pager valueForKey:@"pageControllers"];
@@ -479,4 +742,143 @@ using namespace facebook::react;
479
742
  XCTAssertEqual(removed.count, 0u);
480
743
  }
481
744
 
745
+ - (void)testEmptyPageWithoutReleasedOwnerOrScrollHostDoesNotScheduleRetry
746
+ {
747
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
748
+ [pager attachScrollObserverForCurrentPage];
749
+ XCTAssertNil([pager valueForKey:@"observedScrollView"]);
750
+ XCTAssertFalse([[pager valueForKey:@"scrollResolveRetryScheduled"] boolValue]);
751
+ XCTAssertEqual([[pager valueForKey:@"scrollResolveRetryAttempt"] integerValue], 0);
752
+ }
753
+
754
+ - (void)testLateReplacementRecoversImmediatelyOnReadinessEvenAfterRetryBudgetExpires
755
+ {
756
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
757
+ RNCNativeScrollerComponentView *previous = [self addScrollerToPager:pager atIndex:0];
758
+ previous.tag = 91003;
759
+ [pager attachScrollObserverForCurrentPage];
760
+ UIView *sharedHeader = [pager valueForKey:@"sharedHeaderHostView"];
761
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], previous.scrollView);
762
+
763
+ ShadowView removedView;
764
+ removedView.tag = (Tag)previous.tag;
765
+ MountingTransaction removal(1, 1, {ShadowViewMutation::RemoveMutation(1, removedView, 0)}, {});
766
+ SurfaceTelemetry telemetry;
767
+ [pager mountingTransactionWillMount:removal withSurfaceTelemetry:telemetry];
768
+ [previous removeFromSuperview];
769
+ [self finishMount:pager];
770
+ XCTAssertNil([pager valueForKey:@"observedScrollView"]);
771
+
772
+ // Simulate retry budget exhaustion before the replacement becomes ready.
773
+ [pager setValue:@12 forKey:@"scrollResolveRetryAttempt"];
774
+ [pager setValue:@NO forKey:@"scrollResolveRetryScheduled"];
775
+ NSUInteger oldToken = [[pager valueForKey:@"scrollResolveRetryToken"] unsignedIntegerValue];
776
+ [pager setValue:@(oldToken + 1) forKey:@"scrollResolveRetryToken"];
777
+
778
+ UIViewController *page = [pager valueForKey:@"pageControllers"][0];
779
+ UICollectionView *lateList = [[UICollectionView alloc]
780
+ initWithFrame:CGRectMake(0, 0, 320, 600)
781
+ collectionViewLayout:[UICollectionViewFlowLayout new]];
782
+ [page.view addSubview:lateList];
783
+
784
+ // Readiness notification triggers immediate reconcile even after retry budget expired.
785
+ [[NSNotificationCenter defaultCenter]
786
+ postNotificationName:@"OneKeyNativeScrollViewDidBecomeReady"
787
+ object:lateList];
788
+ XCTAssertEqualObjects([pager valueForKey:@"pendingFallbackScrollView"], lateList);
789
+
790
+ // Explicit scroller readiness attaches synchronously with zero debounce delay.
791
+ [lateList removeFromSuperview];
792
+ RNCNativeScrollerComponentView *lateScroller = [self addScrollerToPager:pager atIndex:0];
793
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], lateScroller.scrollView);
794
+ XCTAssertEqual(sharedHeader.superview, lateScroller.scrollView);
795
+ XCTAssertEqualWithAccuracy(lateScroller.scrollView.contentInset.top, 164, 0.01);
796
+ XCTAssertNil([pager valueForKey:@"releasedPageScrollStates"][@"page-0"]);
797
+ }
798
+
799
+ - (RCTViewComponentView *)addNativeListToPager:(RNCCollapsiblePagerViewComponentView *)pager
800
+ {
801
+ Class nativeListClass = NSClassFromString(@"HybridNativeListComponent");
802
+ XCTAssertNotNil(nativeListClass, @"The real NativeList test dependency must be linked");
803
+ RCTViewComponentView *component = [[nativeListClass alloc] init];
804
+ component.tag = 92001;
805
+ component.frame = CGRectMake(0, 0, 320, 600);
806
+ component.contentView.frame = component.bounds;
807
+ UIViewController *page = [pager valueForKey:@"pageControllers"][0];
808
+ [page.view addSubview:component];
809
+ [self addTeardownBlock:^{
810
+ [component prepareForRecycle];
811
+ [component removeFromSuperview];
812
+ }];
813
+ [component.contentView layoutIfNeeded];
814
+ return component;
815
+ }
816
+
817
+ - (void)testNativeListFooterScrollViewCannotReplacePageOwner
818
+ {
819
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
820
+ RCTViewComponentView *component = [self addNativeListToPager:pager];
821
+ UICollectionView *list = nil;
822
+ for (UIView *child in component.contentView.subviews) {
823
+ if ([child isKindOfClass:UICollectionView.class]) {
824
+ list = (UICollectionView *)child;
825
+ break;
826
+ }
827
+ }
828
+ XCTAssertTrue([list isKindOfClass:UICollectionView.class]);
829
+ XCTAssertGreaterThan(CGRectGetWidth(list.bounds), 0);
830
+ XCTAssertGreaterThan(CGRectGetHeight(list.bounds), 0);
831
+ XCTAssertEqual(list.delegate, (id)component.contentView);
832
+ XCTAssertTrue([list isDescendantOfView:component.contentView]);
833
+ [pager attachScrollObserverForCurrentPage];
834
+ UIView *header = [pager valueForKey:@"sharedHeaderHostView"];
835
+ XCTAssertNotNil(header);
836
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], list);
837
+ XCTAssertEqual(header.superview, list);
838
+ CGFloat inset = list.contentInset.top;
839
+ CGFloat offset = list.contentOffset.y;
840
+
841
+ RCTScrollViewComponentView *footerList = [RCTScrollViewComponentView new];
842
+ footerList.tag = 92002;
843
+ footerList.frame = CGRectMake(0, 0, 320, 400);
844
+ footerList.scrollView.frame = footerList.bounds;
845
+ footerList.scrollView.contentSize = CGSizeMake(320, 1000);
846
+ footerList.scrollView.alwaysBounceHorizontal = NO;
847
+ for (NSInteger index = 0; index < 6; index++) {
848
+ UIView *slot = index == 2 ? footerList : [UIView new];
849
+ [component mountChildComponentView:(id)slot index:index];
850
+ }
851
+ XCTAssertTrue([footerList isDescendantOfView:list]);
852
+ XCTAssertEqual([pager verticalScrollViewForPageAtIndex:0], list);
853
+ ShadowView inserted;
854
+ inserted.tag = (Tag)footerList.tag;
855
+ MountingTransaction transaction(1, 2,
856
+ {ShadowViewMutation::InsertMutation((Tag)component.tag, inserted, 2)}, {});
857
+ SurfaceTelemetry telemetry;
858
+ [pager mountingTransactionDidMount:transaction withSurfaceTelemetry:telemetry];
859
+ XCTAssertEqual([pager valueForKey:@"observedScrollView"], list);
860
+ XCTAssertEqual(header.superview, list);
861
+ XCTAssertEqualWithAccuracy(list.contentInset.top, inset, 0.01);
862
+ XCTAssertEqualWithAccuracy(list.contentOffset.y, offset, 0.01);
863
+ XCTAssertEqualWithAccuracy(footerList.scrollView.contentInset.top, 0, 0.01);
864
+ }
865
+
866
+ - (void)testExplicitScrollerSiblingStillPrecedesNativeList
867
+ {
868
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
869
+ [self addNativeListToPager:pager];
870
+ RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
871
+ XCTAssertEqual([pager verticalScrollViewForPageAtIndex:0], scroller.scrollView);
872
+ }
873
+
874
+ - (void)testPageWithoutNativeListStillDelegatesToNestedPager
875
+ {
876
+ RNCCollapsiblePagerViewComponentView *outer = [self pagerWithPageCount:1];
877
+ RNCCollapsiblePagerViewComponentView *inner = [self pagerWithPageCount:1];
878
+ RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:inner atIndex:0];
879
+ UIViewController *page = [outer valueForKey:@"pageControllers"][0];
880
+ [page.view addSubview:inner];
881
+ XCTAssertEqual([outer verticalScrollViewForPageAtIndex:0], scroller.scrollView);
882
+ }
883
+
482
884
  @end