Skip to content

Commit 71beeeb

Browse files
committed
Add Fabric child retention regression tests
1 parent 234bba0 commit 71beeeb

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

‎packages/react-native/React/Tests/Mounting/RCTViewComponentViewChildMountingTests.mm‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)