@@ -150,6 +150,120 @@ - (void)testUnmountReleasesTrackedChild
150150 [parent unmountChildComponentView: child index: 0 ];
151151 }
152152 XCTAssertNil (weakChild);
153+ // Keep the parent alive so its deallocation cannot hide a retained child.
154+ XCTAssertEqual (parent.subviews .count , 0u );
155+ }
156+
157+ - (void )testUnmountReleasesClippedChild
158+ {
159+ RCTViewComponentView *parent = [[RCTViewComponentView alloc ] initWithFrame: CGRectMake (0 , 0 , 100 , 100 )];
160+ __weak RCTViewComponentView *weakChild;
161+ @autoreleasepool {
162+ RCTViewComponentView *child = [[RCTViewComponentView alloc ] initWithFrame: CGRectMake (0 , 200 , 50 , 50 )];
163+ weakChild = child;
164+ [parent mountChildComponentView: child index: 0 ];
165+
166+ auto props = std::make_shared<ViewProps>();
167+ props->removeClippedSubviews = true ;
168+ [parent updateProps: props oldProps: parent.props];
169+ [parent updateClippedSubviewsWithClipRect: parent.bounds relativeToView: parent];
170+ XCTAssertNil (child.superview );
171+ }
172+
173+ @autoreleasepool {
174+ // Clipping keeps the logical child alive until Fabric unmounts it.
175+ XCTAssertNotNil (weakChild);
176+ [parent unmountChildComponentView: weakChild index: 0 ];
177+ }
178+ XCTAssertNil (weakChild);
179+ XCTAssertEqual (parent.subviews .count , 0u );
180+ }
181+
182+ - (void )testUnmountReleasesChildrenAfterRepeatedClippingToggles
183+ {
184+ RCTViewComponentView *parent = [[RCTViewComponentView alloc ] initWithFrame: CGRectMake (0 , 0 , 100 , 100 )];
185+ auto clippingProps = std::make_shared<ViewProps>();
186+ clippingProps->removeClippedSubviews = true ;
187+ auto defaultProps = std::make_shared<ViewProps>();
188+
189+ for (NSInteger iteration = 0 ; iteration < 3 ; iteration++) {
190+ __weak RCTViewComponentView *weakChild;
191+ @autoreleasepool {
192+ RCTViewComponentView *child = [[RCTViewComponentView alloc ] initWithFrame: CGRectMake (0 , 200 , 50 , 50 )];
193+ weakChild = child;
194+ [parent mountChildComponentView: child index: 0 ];
195+ [parent updateProps: clippingProps oldProps: parent.props];
196+ [parent updateClippedSubviewsWithClipRect: parent.bounds relativeToView: parent];
197+ XCTAssertNil (child.superview );
198+ }
199+
200+ @autoreleasepool {
201+ XCTAssertNotNil (weakChild);
202+ [parent updateProps: defaultProps oldProps: parent.props];
203+ XCTAssertEqual (weakChild.superview , parent);
204+ [parent unmountChildComponentView: weakChild index: 0 ];
205+ }
206+ XCTAssertNil (weakChild, @" Child retained after clipping toggle %ld " , (long )iteration);
207+ XCTAssertEqual (parent.subviews .count , 0u );
208+ }
209+ }
210+
211+ - (void )testClippingDoesNotRetainRemovedNativeSubview
212+ {
213+ RCTViewComponentView *parent = [RCTViewComponentView new ];
214+ RCTViewComponentView *child = [RCTViewComponentView new ];
215+ [parent mountChildComponentView: child index: 0 ];
216+
217+ __weak UIView *weakEffect;
218+ @autoreleasepool {
219+ UIView *effect = [UIView new ];
220+ weakEffect = effect;
221+ [parent insertSubview: effect atIndex: 0 ];
222+
223+ auto props = std::make_shared<ViewProps>();
224+ props->removeClippedSubviews = true ;
225+ [parent updateProps: props oldProps: parent.props];
226+ [effect removeFromSuperview ];
227+ }
228+
229+ XCTAssertNil (weakEffect);
230+ [parent updateProps: std: :make_shared<ViewProps>() oldProps: parent.props];
231+ XCTAssertEqualObjects (parent.subviews , (@[ child ]));
232+ }
233+
234+ - (void )testPrepareForRecycleReleasesClippedChildren
235+ {
236+ RCTViewComponentView *parent = [[RCTViewComponentView alloc ] initWithFrame: CGRectMake (0 , 0 , 100 , 100 )];
237+ __weak RCTViewComponentView *weakChild;
238+ @autoreleasepool {
239+ RCTViewComponentView *child = [[RCTViewComponentView alloc ] initWithFrame: CGRectMake (0 , 200 , 50 , 50 )];
240+ weakChild = child;
241+ [parent mountChildComponentView: child index: 0 ];
242+
243+ auto props = std::make_shared<ViewProps>();
244+ props->removeClippedSubviews = true ;
245+ [parent updateProps: props oldProps: parent.props];
246+ [parent updateClippedSubviewsWithClipRect: parent.bounds relativeToView: parent];
247+ XCTAssertNil (child.superview );
248+ }
249+
250+ @autoreleasepool {
251+ XCTAssertNotNil (weakChild);
252+ [parent prepareForRecycle ];
253+ }
254+ XCTAssertNil (weakChild);
255+
256+ // Reusing the parent must not retain children from its previous lifecycle.
257+ __weak RCTViewComponentView *weakNewChild;
258+ @autoreleasepool {
259+ RCTViewComponentView *child = [RCTViewComponentView new ];
260+ weakNewChild = child;
261+ [parent mountChildComponentView: child index: 0 ];
262+ XCTAssertEqualObjects (parent.subviews , (@[ child ]));
263+ [parent unmountChildComponentView: child index: 0 ];
264+ }
265+ XCTAssertNil (weakNewChild);
266+ XCTAssertEqual (parent.subviews .count , 0u );
153267}
154268
155269@end
0 commit comments