Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -243,21 +243,24 @@ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompo
if (_removeClippedSubviews) {
[_reactSubviews removeObjectAtIndex:index];
} else {
// This getter can migrate children from a recycled clipping container.
// Reconcile before reading superview, even when assertions are disabled.
[[maybe_unused]] UIView *containerView = self.currentContainerView;
RCTAssert(
childComponentView.superview != nil,
@"Attempt to unmount a view which is not mounted. (parent: %@, child: %@, index: %@)",
self,
childComponentView,
@(index));
RCTAssert(
childComponentView.superview == self.currentContainerView,
childComponentView.superview == containerView,
@"Attempt to unmount a view which is mounted inside a different view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));
#ifndef NS_BLOCK_ASSERTIONS
NSArray<UIView *> *containerSubviews = self.currentContainerView.subviews;
NSArray<UIView *> *containerSubviews = containerView.subviews;
BOOL isIndexInBounds = index >= 0 && (NSUInteger)index < containerSubviews.count;
RCTAssert(
isIndexInBounds && [containerSubviews objectAtIndex:index] == childComponentView,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#import <React/RCTAssert.h>
#import <React/RCTViewComponentView.h>
#import <XCTest/XCTest.h>
#import <react/renderer/components/view/ViewProps.h>

using namespace facebook::react;

@interface RCTViewComponentViewUnmountTests : XCTestCase
@end
Expand Down Expand Up @@ -83,4 +86,54 @@ - (void)testUnmountWithInBoundsMismatchStillReportsTagAtIndex
XCTAssertNotEqualObjects(thrownName, NSRangeException);
}

// A recycled view can receive children before its own props are updated. If
// the new owner is zero-sized, layer invalidation does not reconcile the old
// clipping container. Unmount must do so before checking the child's parent.
- (void)testUnmountReconcilesRecycledContainerAtZeroSize
{
RCTViewComponentView *parent = [[RCTViewComponentView alloc] initWithFrame:CGRectZero];
auto clippingProps = std::make_shared<ViewProps>();
clippingProps->yogaStyle.setOverflow(facebook::yoga::Overflow::Hidden);
clippingProps->outlineWidth = 1;
[parent updateProps:clippingProps oldProps:parent.props];
[parent finalizeUpdates:RNComponentViewUpdateMaskAll];

RCTViewComponentView *previousChild = [RCTViewComponentView new];
[parent mountChildComponentView:previousChild index:0];
UIView *previousContainer = previousChild.superview;
XCTAssertNotEqual(previousContainer, parent);
XCTAssertNotNil(previousContainer);
[parent unmountChildComponentView:previousChild index:0];
[parent prepareForRecycle];

// Fabric mounts descendants before applying the recycled parent's new props.
RCTViewComponentView *child = [RCTViewComponentView new];
[parent mountChildComponentView:child index:0];
[parent updateProps:std::make_shared<ViewProps>() oldProps:parent.props];
[parent finalizeUpdates:RNComponentViewUpdateMaskAll];
XCTAssertTrue(CGSizeEqualToSize(parent.bounds.size, CGSizeZero));
XCTAssertEqual(child.superview, previousContainer);

NSString *message = nil;
NSString *thrownName = RCTUnmountAndReportAssert(parent, child, 0, &message);

XCTAssertNil(message, @"Container migration must precede the ownership check");
XCTAssertNil(thrownName);
XCTAssertNil(child.superview);
XCTAssertNil(previousContainer.superview, @"The stale container must be removed");
}

- (void)testUnmountStillReportsChildMountedUnderAnotherParent
{
RCTViewComponentView *parent = [RCTViewComponentView new];
RCTViewComponentView *otherParent = [RCTViewComponentView new];
RCTViewComponentView *child = [RCTViewComponentView new];
[otherParent mountChildComponentView:child index:0];

NSString *message = nil;
RCTUnmountAndReportAssert(parent, child, 0, &message);

XCTAssertTrue([message containsString:@"mounted inside a different view"]);
}

@end
Loading