Skip to content

Commit 48206fe

Browse files
committed
[iOS] Clear DevLoadingView when its host is invalidated
1 parent 27e7721 commit 48206fe

3 files changed

Lines changed: 166 additions & 1 deletion

File tree

‎packages/react-native/React/CoreModules/RCTDevLoadingView.mm‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ @implementation RCTDevLoadingView {
3535
UIButton *_dismissButton;
3636
NSDate *_showDate;
3737
BOOL _hiding;
38+
BOOL _invalidated;
3839
dispatch_block_t _initialMessageBlock;
3940
}
4041

@@ -59,6 +60,20 @@ - (instancetype)init
5960
return self;
6061
}
6162

63+
- (void)invalidate
64+
{
65+
RCTExecuteOnMainQueue(^{
66+
self->_invalidated = YES;
67+
// The delayed message retains this module, so dealloc cannot cancel it when the host is invalidated.
68+
[self clearInitialMessageDelay];
69+
self->_window.hidden = YES;
70+
self->_window = nil;
71+
self->_container = nil;
72+
self->_label = nil;
73+
self->_dismissButton = nil;
74+
});
75+
}
76+
6277
- (void)dealloc
6378
{
6479
[self clearInitialMessageDelay];
@@ -127,7 +142,7 @@ - (void)showMessage:(NSString *)message
127142
}
128143

129144
dispatch_async(dispatch_get_main_queue(), ^{
130-
if (RCTRunningInTestEnvironment()) {
145+
if (self->_invalidated || [self isRunningInTestEnvironment]) {
131146
return;
132147
}
133148

@@ -324,6 +339,11 @@ - (void)showOfflineMessage
324339
[self showMessage:message color:color backgroundColor:backgroundColor dismissButton:false];
325340
}
326341

342+
- (BOOL)isRunningInTestEnvironment
343+
{
344+
return RCTRunningInTestEnvironment();
345+
}
346+
327347
- (BOOL)isDarkModeEnabled
328348
{
329349
// We pass nil here to match the behavior of the native module.

‎packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
E7DB218C22B41FCD005AC45F /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB218B22B41FCD005AC45F /* XCTest.framework */; };
5757
F0D621C32BBB9E38005960AC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */; };
5858
F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */; };
59+
D1A60AD11B9247A080734F02 /* RCTDevLoadingViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */; };
5960
/* End PBXBuildFile section */
6061

6162
/* Begin PBXContainerItemProxy section */
@@ -163,6 +164,7 @@
163164
E7DB218B22B41FCD005AC45F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = XCTest.framework; sourceTree = DEVELOPER_DIR; };
164165
F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
165166
F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleArrayBufferTests.mm; sourceTree = "<group>"; };
167+
D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTDevLoadingViewTests.m; sourceTree = "<group>"; };
166168
/* End PBXFileReference section */
167169

168170
/* Begin PBXFrameworksBuildPhase section */
@@ -328,6 +330,7 @@
328330
E7DB20CA22B2BAA5005AC45F /* RCTConvert_NSURLTests.m */,
329331
E7DB20CE22B2BAA5005AC45F /* RCTConvert_YGValueTests.m */,
330332
383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */,
333+
D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */,
331334
E7DB20C822B2BAA5005AC45F /* RCTDevMenuTests.m */,
332335
E7DB20AF22B2BAA4005AC45F /* RCTFontTests.m */,
333336
E7DB20C722B2BAA5005AC45F /* RCTFormatErrorTests.m */,
@@ -770,6 +773,7 @@
770773
E7DB20E722B2BAA6005AC45F /* RCTConvert_NSURLTests.m in Sources */,
771774
E7DB20EA22B2BAA6005AC45F /* RCTImageUtilTests.m in Sources */,
772775
E7DB20D722B2BAA6005AC45F /* RCTModuleInitTests.m in Sources */,
776+
D1A60AD11B9247A080734F02 /* RCTDevLoadingViewTests.m in Sources */,
773777
E7DB20E522B2BAA6005AC45F /* RCTDevMenuTests.m in Sources */,
774778
E7DB20DE22B2BAA6005AC45F /* RCTUnicodeDecodeTests.m in Sources */,
775779
CD10C7A5290BD4EB0033E1ED /* RCTEventEmitterTests.m in Sources */,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <React/RCTDevLoadingView.h>
9+
#import <React/RCTDevLoadingViewSetEnabled.h>
10+
#import <React/RCTJavaScriptLoader.h>
11+
#import <React/RCTUtils.h>
12+
#import <XCTest/XCTest.h>
13+
14+
#if RCT_DEV_MENU
15+
16+
@interface RCTDevLoadingView (Tests)
17+
- (void)invalidate;
18+
- (BOOL)isRunningInTestEnvironment;
19+
@end
20+
21+
@interface RCTDevLoadingViewForTests : RCTDevLoadingView
22+
@end
23+
24+
@implementation RCTDevLoadingViewForTests
25+
- (BOOL)isRunningInTestEnvironment
26+
{
27+
// Exercise the real display path instead of the XCTest early return.
28+
return NO;
29+
}
30+
@end
31+
32+
@interface RCTDevLoadingViewTests : XCTestCase
33+
@end
34+
35+
@implementation RCTDevLoadingViewTests {
36+
BOOL _wasEnabled;
37+
NSMutableArray<RCTDevLoadingView *> *_loadingViews;
38+
}
39+
40+
- (void)setUp
41+
{
42+
[super setUp];
43+
XCTAssertTrue(RCTIsMainQueue());
44+
_wasEnabled = RCTDevLoadingViewGetEnabled();
45+
RCTDevLoadingViewSetEnabled(YES);
46+
_loadingViews = [NSMutableArray new];
47+
}
48+
49+
- (void)tearDown
50+
{
51+
for (RCTDevLoadingView *loadingView in _loadingViews) {
52+
[loadingView invalidate];
53+
}
54+
[self drainMainQueue];
55+
_loadingViews = nil;
56+
RCTDevLoadingViewSetEnabled(_wasEnabled);
57+
[super tearDown];
58+
}
59+
60+
- (RCTDevLoadingView *)newLoadingView
61+
{
62+
RCTDevLoadingView *loadingView = [RCTDevLoadingViewForTests new];
63+
[_loadingViews addObject:loadingView];
64+
return loadingView;
65+
}
66+
67+
- (void)drainMainQueue
68+
{
69+
XCTestExpectation *drained = [self expectationWithDescription:@"Queued loading messages finished"];
70+
dispatch_async(dispatch_get_main_queue(), ^{
71+
// updateProgress enqueues another display block from its main-queue callback.
72+
dispatch_async(dispatch_get_main_queue(), ^{
73+
[drained fulfill];
74+
});
75+
});
76+
[self waitForExpectations:@[ drained ] timeout:2];
77+
}
78+
79+
- (void)showMessage:(RCTDevLoadingView *)loadingView
80+
{
81+
[loadingView showMessage:@"Loading from Metro…"
82+
color:UIColor.whiteColor
83+
backgroundColor:UIColor.blackColor
84+
dismissButton:YES];
85+
}
86+
87+
- (void)testInvalidateClearsVisibleWindow
88+
{
89+
RCTDevLoadingView *loadingView = [self newLoadingView];
90+
[self showMessage:loadingView];
91+
[self drainMainQueue];
92+
93+
UIWindow *window = [loadingView valueForKey:@"window"];
94+
XCTAssertNotNil(window);
95+
XCTAssertFalse(window.hidden);
96+
XCTAssertNotNil([loadingView valueForKey:@"dismissButton"]);
97+
98+
[loadingView invalidate];
99+
[self drainMainQueue];
100+
101+
XCTAssertTrue(window.hidden);
102+
for (NSString *key in @[ @"window", @"container", @"label", @"dismissButton" ]) {
103+
XCTAssertNil([loadingView valueForKey:key], @"%@ should be released during invalidation", key);
104+
}
105+
}
106+
107+
- (void)testInvalidateCancelsPendingInitialMessage
108+
{
109+
RCTDevLoadingView *loadingView = [self newLoadingView];
110+
[loadingView showWithURL:[NSURL URLWithString:@"http://localhost:8081/index.bundle"]];
111+
dispatch_block_t pendingMessage = [loadingView valueForKey:@"initialMessageBlock"];
112+
XCTAssertNotNil(pendingMessage);
113+
XCTAssertEqual(dispatch_block_testcancel(pendingMessage), 0L);
114+
115+
[loadingView invalidate];
116+
[self drainMainQueue];
117+
118+
XCTAssertNil([loadingView valueForKey:@"initialMessageBlock"]);
119+
XCTAssertNotEqual(dispatch_block_testcancel(pendingMessage), 0L);
120+
}
121+
122+
- (void)testInvalidatePreventsQueuedMessagesFromRecreatingWindow
123+
{
124+
RCTDevLoadingView *messageView = [self newLoadingView];
125+
RCTDevLoadingView *progressView = [self newLoadingView];
126+
RCTLoadingProgress *progress = [RCTLoadingProgress new];
127+
progress.status = @"Downloading";
128+
129+
[self showMessage:messageView];
130+
[progressView updateProgress:progress];
131+
[messageView invalidate];
132+
[progressView invalidate];
133+
[self drainMainQueue];
134+
135+
XCTAssertNil([messageView valueForKey:@"window"]);
136+
XCTAssertNil([progressView valueForKey:@"window"]);
137+
}
138+
139+
@end
140+
141+
#endif

0 commit comments

Comments
 (0)