|
5 | 5 | * LICENSE file in the root directory of this source tree. |
6 | 6 | */ |
7 | 7 |
|
| 8 | +#import <React/RCTScrollViewComponentView.h> |
8 | 9 | #import <React/RCTViewComponentView.h> |
9 | 10 | #import <XCTest/XCTest.h> |
| 11 | +#import <react/renderer/components/scrollview/ScrollViewShadowNode.h> |
10 | 12 | #import <react/renderer/components/view/ViewProps.h> |
11 | 13 | #import <react/renderer/components/view/ViewShadowNode.h> |
12 | 14 | #import <react/renderer/graphics/Color.h> |
|
20 | 22 | return props; |
21 | 23 | } |
22 | 24 |
|
23 | | -// Defined in RCTViewComponentView.mm. |
24 | | -NSUInteger RCTViewComponentViewClippingViewCount(void); |
| 25 | +// The ScrollView's clipping pass, which runs after each mounting transaction and while scrolling. |
| 26 | +@interface RCTScrollViewComponentView (Testing) |
| 27 | +- (void)_remountChildren; |
| 28 | +@end |
| 29 | + |
| 30 | +// Counts the clipping passes that reach it. |
| 31 | +@interface RCTClippingPassSpyView : UIView |
| 32 | +@property (nonatomic, assign) NSUInteger passCount; |
| 33 | +@end |
| 34 | + |
| 35 | +@implementation RCTClippingPassSpyView |
| 36 | +- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView |
| 37 | +{ |
| 38 | + self.passCount++; |
| 39 | +} |
| 40 | +@end |
| 41 | + |
| 42 | +// A ScrollView, 400 points tall, with `content` as its content view. |
| 43 | +static RCTScrollViewComponentView *makeScrollView(UIView *content) |
| 44 | +{ |
| 45 | + RCTScrollViewComponentView *scrollView = |
| 46 | + [[RCTScrollViewComponentView alloc] initWithFrame:CGRectMake(0, 0, 100, 400)]; |
| 47 | + [scrollView mountChildComponentView:(id)content index:0]; |
| 48 | + return scrollView; |
| 49 | +} |
25 | 50 |
|
26 | 51 | @interface RCTViewComponentViewTests : XCTestCase |
27 | 52 | @end |
@@ -166,46 +191,150 @@ - (void)testRemoveClippedSubviewsIsKeptOnRecycledView |
166 | 191 | XCTAssertEqual(reactSubviews.count, 1u); |
167 | 192 | } |
168 | 193 |
|
169 | | -#pragma mark - Clipping view count |
170 | | - |
171 | | -// The count is shared by all views, so each test compares it against its value at the start. |
| 194 | +#pragma mark - ScrollView clipping pass |
172 | 195 |
|
173 | | -- (void)testClippingViewCountFollowsRemoveClippedSubviews |
| 196 | +- (void)testScrollViewSkipsClippingPassWithoutClippingViews |
174 | 197 | { |
175 | | - NSUInteger initialCount = RCTViewComponentViewClippingViewCount(); |
176 | | - RCTViewComponentView *first = [RCTViewComponentView new]; |
177 | | - RCTViewComponentView *second = [RCTViewComponentView new]; |
| 198 | + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 400)]; |
| 199 | + RCTClippingPassSpyView *spy = [RCTClippingPassSpyView new]; |
| 200 | + RCTScrollViewComponentView *scrollView = makeScrollView(spy); |
| 201 | + [window addSubview:scrollView]; |
178 | 202 |
|
179 | | - [first updateProps:makeViewProps(true) oldProps:ViewShadowNode::defaultSharedProps()]; |
180 | | - [second updateProps:makeViewProps(true) oldProps:ViewShadowNode::defaultSharedProps()]; |
181 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount + 2); |
| 203 | + [scrollView _remountChildren]; |
| 204 | + XCTAssertEqual(spy.passCount, 0u); |
| 205 | +} |
182 | 206 |
|
183 | | - // Props that keep `removeClippedSubviews` on don't count the view again. |
184 | | - [first updateProps:makeViewProps(true) oldProps:nullptr]; |
185 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount + 2); |
| 207 | +- (void)testScrollViewOutsideWindowRunsClippingPass |
| 208 | +{ |
| 209 | + // Content can be mounted before its ScrollView is in a window, so the pass runs there as before. |
| 210 | + RCTClippingPassSpyView *spy = [RCTClippingPassSpyView new]; |
| 211 | + RCTScrollViewComponentView *scrollView = makeScrollView(spy); |
186 | 212 |
|
187 | | - [first updateProps:makeViewProps(false) oldProps:nullptr]; |
188 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount + 1); |
189 | | - [second updateProps:makeViewProps(false) oldProps:nullptr]; |
190 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount); |
| 213 | + [scrollView _remountChildren]; |
| 214 | + XCTAssertEqual(spy.passCount, 1u); |
| 215 | +} |
| 216 | + |
| 217 | +- (void)testClippingViewInWindowGetsClippingPass |
| 218 | +{ |
| 219 | + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 400)]; |
| 220 | + RCTViewComponentView *content = [RCTViewComponentView new]; |
| 221 | + content.frame = CGRectMake(0, 0, 100, 2000); |
| 222 | + RCTClippingPassSpyView *spy = [RCTClippingPassSpyView new]; |
| 223 | + [content mountChildComponentView:(id)spy index:0]; |
| 224 | + |
| 225 | + // Mount the rows before turning clipping on, so that both start attached. |
| 226 | + RCTViewComponentView *list = [RCTViewComponentView new]; |
| 227 | + list.frame = CGRectMake(0, 0, 100, 2000); |
| 228 | + UIView *visibleRow = [UIView new]; |
| 229 | + visibleRow.frame = CGRectMake(0, 0, 100, 100); |
| 230 | + UIView *offscreenRow = [UIView new]; |
| 231 | + offscreenRow.frame = CGRectMake(0, 1500, 100, 100); |
| 232 | + [list mountChildComponentView:(id)visibleRow index:0]; |
| 233 | + [list mountChildComponentView:(id)offscreenRow index:1]; |
| 234 | + [list updateProps:makeViewProps(true) oldProps:ViewShadowNode::defaultSharedProps()]; |
| 235 | + [content mountChildComponentView:list index:1]; |
| 236 | + XCTAssertEqual(offscreenRow.superview, list); |
| 237 | + |
| 238 | + RCTScrollViewComponentView *scrollView = makeScrollView(content); |
| 239 | + [window addSubview:scrollView]; |
| 240 | + |
| 241 | + [scrollView _remountChildren]; |
| 242 | + XCTAssertEqual(spy.passCount, 1u); |
| 243 | + XCTAssertEqual(visibleRow.superview, list); |
| 244 | + XCTAssertNil(offscreenRow.superview); |
191 | 245 | } |
192 | 246 |
|
193 | | -- (void)testClippingViewCountAfterRecycleAndDealloc |
| 247 | +- (void)testClippingPassFollowsClippingViewProps |
194 | 248 | { |
195 | | - NSUInteger initialCount = RCTViewComponentViewClippingViewCount(); |
196 | | - @autoreleasepool { |
197 | | - RCTViewComponentView *view = [RCTViewComponentView new]; |
198 | | - [view updateProps:makeViewProps(true) oldProps:ViewShadowNode::defaultSharedProps()]; |
199 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount + 1); |
| 249 | + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 400)]; |
| 250 | + RCTViewComponentView *content = [RCTViewComponentView new]; |
| 251 | + RCTClippingPassSpyView *spy = [RCTClippingPassSpyView new]; |
| 252 | + [content mountChildComponentView:(id)spy index:0]; |
| 253 | + RCTViewComponentView *list = [RCTViewComponentView new]; |
| 254 | + [list updateProps:makeViewProps(true) oldProps:ViewShadowNode::defaultSharedProps()]; |
| 255 | + [content mountChildComponentView:list index:1]; |
| 256 | + RCTScrollViewComponentView *scrollView = makeScrollView(content); |
| 257 | + [window addSubview:scrollView]; |
| 258 | + |
| 259 | + [scrollView _remountChildren]; |
| 260 | + XCTAssertEqual(spy.passCount, 1u); |
| 261 | + |
| 262 | + [list updateProps:makeViewProps(false) oldProps:nullptr]; |
| 263 | + [scrollView _remountChildren]; |
| 264 | + XCTAssertEqual(spy.passCount, 1u); |
| 265 | + |
| 266 | + [list updateProps:makeViewProps(true) oldProps:nullptr]; |
| 267 | + [scrollView _remountChildren]; |
| 268 | + XCTAssertEqual(spy.passCount, 2u); |
| 269 | + |
| 270 | + // A clipping view outside the window doesn't need the pass. |
| 271 | + [content unmountChildComponentView:list index:1]; |
| 272 | + [scrollView _remountChildren]; |
| 273 | + XCTAssertEqual(spy.passCount, 2u); |
| 274 | + |
| 275 | + [content mountChildComponentView:list index:1]; |
| 276 | + [scrollView _remountChildren]; |
| 277 | + XCTAssertEqual(spy.passCount, 3u); |
| 278 | + |
| 279 | + // A recycled view starts without `removeClippedSubviews`. |
| 280 | + [content unmountChildComponentView:list index:1]; |
| 281 | + [list prepareForRecycle]; |
| 282 | + [content mountChildComponentView:list index:1]; |
| 283 | + [scrollView _remountChildren]; |
| 284 | + XCTAssertEqual(spy.passCount, 3u); |
| 285 | + |
| 286 | + // Turned on outside the window, the view registers once it is attached. |
| 287 | + [content unmountChildComponentView:list index:1]; |
| 288 | + [list updateProps:makeViewProps(true) oldProps:nullptr]; |
| 289 | + [content mountChildComponentView:list index:1]; |
| 290 | + [scrollView _remountChildren]; |
| 291 | + XCTAssertEqual(spy.passCount, 4u); |
| 292 | +} |
200 | 293 |
|
201 | | - [view prepareForRecycle]; |
202 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount); |
| 294 | +- (void)testClippingViewOnlyRegistersWithNearestScrollView |
| 295 | +{ |
| 296 | + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 400)]; |
| 297 | + RCTViewComponentView *innerContent = [RCTViewComponentView new]; |
| 298 | + RCTClippingPassSpyView *innerSpy = [RCTClippingPassSpyView new]; |
| 299 | + [innerContent mountChildComponentView:(id)innerSpy index:0]; |
| 300 | + RCTViewComponentView *list = [RCTViewComponentView new]; |
| 301 | + [list updateProps:makeViewProps(true) oldProps:ViewShadowNode::defaultSharedProps()]; |
| 302 | + [innerContent mountChildComponentView:list index:1]; |
| 303 | + RCTScrollViewComponentView *innerScrollView = makeScrollView(innerContent); |
| 304 | + |
| 305 | + RCTViewComponentView *content = [RCTViewComponentView new]; |
| 306 | + RCTClippingPassSpyView *spy = [RCTClippingPassSpyView new]; |
| 307 | + [content mountChildComponentView:(id)spy index:0]; |
| 308 | + [content mountChildComponentView:innerScrollView index:1]; |
| 309 | + RCTScrollViewComponentView *outerScrollView = makeScrollView(content); |
| 310 | + [window addSubview:outerScrollView]; |
| 311 | + |
| 312 | + // The outer pass stops at the inner ScrollView, which handles `list` itself. |
| 313 | + [outerScrollView _remountChildren]; |
| 314 | + XCTAssertEqual(spy.passCount, 0u); |
| 315 | + [innerScrollView _remountChildren]; |
| 316 | + XCTAssertEqual(innerSpy.passCount, 1u); |
| 317 | +} |
203 | 318 |
|
204 | | - [view updateProps:makeViewProps(true) oldProps:nullptr]; |
205 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount + 1); |
206 | | - } |
207 | | - // The view was deallocated with `removeClippedSubviews` on. |
208 | | - XCTAssertEqual(RCTViewComponentViewClippingViewCount(), initialCount); |
| 319 | +- (void)testScrollViewWithRemoveClippedSubviewsDoesNotRegisterWithOuterScrollView |
| 320 | +{ |
| 321 | + // For example a horizontal list inside a vertical one: ScrollView forwards the prop to the |
| 322 | + // native ScrollView too, but the inner ScrollView clips its own content. |
| 323 | + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 400)]; |
| 324 | + RCTScrollViewComponentView *innerScrollView = makeScrollView([RCTViewComponentView new]); |
| 325 | + auto innerProps = std::make_shared<ScrollViewProps>(); |
| 326 | + innerProps->removeClippedSubviews = true; |
| 327 | + [innerScrollView updateProps:innerProps oldProps:ScrollViewShadowNode::defaultSharedProps()]; |
| 328 | + |
| 329 | + RCTViewComponentView *content = [RCTViewComponentView new]; |
| 330 | + RCTClippingPassSpyView *spy = [RCTClippingPassSpyView new]; |
| 331 | + [content mountChildComponentView:(id)spy index:0]; |
| 332 | + [content mountChildComponentView:innerScrollView index:1]; |
| 333 | + RCTScrollViewComponentView *outerScrollView = makeScrollView(content); |
| 334 | + [window addSubview:outerScrollView]; |
| 335 | + |
| 336 | + [outerScrollView _remountChildren]; |
| 337 | + XCTAssertEqual(spy.passCount, 0u); |
209 | 338 | } |
210 | 339 |
|
211 | 340 | #pragma mark - hitTest against non-invertible transforms (#50797) |
|
0 commit comments