Browse Source

Fix merge options leaks to next screen in stack (#5859)

* Fix merge options leaks to next screen in stack

* Rename mergeChild:options to mergeChildOptions:options:

* Rename mergeChildOptions:options: to mergeChildOptions:child:
Yogev Ben David 4 years ago
parent
commit
386bf65d25
No account linked to committer's email address

+ 13
- 0
e2e/Options.test.js View File

@@ -63,6 +63,19 @@ describe('Options', () => {
63 63
     await expect(elementByLabel('Styling Options')).toBeVisible();
64 64
   });
65 65
 
66
+  it('Merging options to invisible component in stack should not affect current component', async () => {
67
+    await elementById(TestIDs.PUSH_BTN).tap();
68
+    await elementById(TestIDs.HIDE_PREVIOUS_SCREEN_TOP_BAR).tap();
69
+    await expect(elementByLabel('Pushed Screen')).toBeVisible();
70
+  });
71
+
72
+  it('Merging options to invisible component should affect the invisible component', async () => {
73
+    await elementById(TestIDs.PUSH_BTN).tap();
74
+    await elementById(TestIDs.HIDE_PREVIOUS_SCREEN_TOP_BAR).tap();
75
+    await elementById(TestIDs.POP_BTN).tap();
76
+    await expect(elementByLabel('Styling Options')).toBeNotVisible();
77
+  });
78
+
66 79
   xit('hides topBar onScroll down and shows it on scroll up', async () => {
67 80
     await elementById(TestIDs.PUSH_OPTIONS_BUTTON).tap();
68 81
     await elementById(TestIDs.SCROLLVIEW_SCREEN_BUTTON).tap();

+ 2
- 0
lib/ios/RNNLayoutProtocol.h View File

@@ -29,6 +29,8 @@ typedef void (^RNNReactViewReadyCompletionBlock)(void);
29 29
 
30 30
 - (void)mergeOptions:(RNNNavigationOptions *)options;
31 31
 
32
+- (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewController *)child;
33
+
32 34
 - (RNNNavigationOptions *)resolveOptions;
33 35
 
34 36
 - (void)setDefaultOptions:(RNNNavigationOptions *)defaultOptions;

+ 8
- 0
lib/ios/RNNStackController.m View File

@@ -1,5 +1,6 @@
1 1
 #import "RNNStackController.h"
2 2
 #import "RNNComponentViewController.h"
3
+#import "UIViewController+Utils.h"
3 4
 
4 5
 @implementation RNNStackController {
5 6
     UIViewController* _presentedViewController;
@@ -21,6 +22,13 @@
21 22
 	[self.presenter applyOptionsOnViewDidLayoutSubviews:self.resolveOptions];
22 23
 }
23 24
 
25
+- (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewController *)child {
26
+    if (child.isLastInStack) {
27
+        [self.presenter mergeOptions:options resolvedOptions:self.resolveOptions];
28
+    }
29
+    [self.parentViewController mergeChildOptions:options child:child];
30
+}
31
+
24 32
 - (UINavigationController *)navigationController {
25 33
 	return self;
26 34
 }

+ 1
- 1
lib/ios/UIViewController+LayoutProtocol.h View File

@@ -16,7 +16,7 @@ typedef void (^RNNReactViewReadyCompletionBlock)(void);
16 16
 
17 17
 - (void)mergeOptions:(RNNNavigationOptions *)options;
18 18
 
19
-- (void)mergeChildOptions:(RNNNavigationOptions *)options;
19
+- (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewController *)child;
20 20
 
21 21
 - (RNNNavigationOptions *)resolveOptions;
22 22
 

+ 3
- 3
lib/ios/UIViewController+LayoutProtocol.m View File

@@ -31,12 +31,12 @@
31 31
 - (void)mergeOptions:(RNNNavigationOptions *)options {
32 32
     [self.options overrideOptions:options];
33 33
     [self.presenter mergeOptions:options resolvedOptions:self.resolveOptions];
34
-    [self.parentViewController mergeChildOptions:options];
34
+    [self.parentViewController mergeChildOptions:options child:self];
35 35
 }
36 36
 
37
-- (void)mergeChildOptions:(RNNNavigationOptions *)options {
37
+- (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewController *)child {
38 38
     [self.presenter mergeOptions:options resolvedOptions:self.resolveOptions];
39
-	[self.parentViewController mergeChildOptions:options];
39
+    [self.parentViewController mergeChildOptions:options child:child];
40 40
 }
41 41
 
42 42
 - (RNNNavigationOptions *)resolveOptions {

+ 4
- 1
lib/ios/Utils/UIViewController+Utils.h View File

@@ -2,5 +2,8 @@
2 2
 #import <UIKit/UIKit.h>
3 3
 
4 4
 @interface UIViewController (Utils)
5
+
5 6
 - (void)forEachChild:(void (^)(UIViewController *child))perform;
6
-@end
7
+- (BOOL)isLastInStack;
8
+
9
+@end

+ 6
- 1
lib/ios/Utils/UIViewController+Utils.m View File

@@ -1,10 +1,15 @@
1 1
 #import "UIViewController+Utils.h"
2 2
 
3 3
 @implementation UIViewController (Utils)
4
+
4 5
 - (void)forEachChild:(void (^)(UIViewController *child))perform {
5 6
     for (UIViewController *child in [self childViewControllers]) {
6 7
         perform(child);
7 8
     }
8 9
 }
9 10
 
10
-@end
11
+- (BOOL)isLastInStack {
12
+    return self.navigationController.childViewControllers.lastObject == self;
13
+}
14
+
15
+@end

playground/ios/NavigationTests/RNNNavigationControllerTest.m → playground/ios/NavigationTests/RNNStackControllerTest.m View File

@@ -4,13 +4,13 @@
4 4
 #import <ReactNativeNavigation/RNNComponentViewController.h>
5 5
 #import "RNNTestRootViewCreator.h"
6 6
 
7
-@interface RNNNavigationControllerTest : XCTestCase
7
+@interface RNNStackControllerTest : XCTestCase
8 8
 
9 9
 @property (nonatomic, strong) RNNStackController *uut;
10 10
 
11 11
 @end
12 12
 
13
-@implementation RNNNavigationControllerTest {
13
+@implementation RNNStackControllerTest {
14 14
 	RNNComponentViewController* _vc1;
15 15
 	id _vc2Mock;
16 16
 	RNNComponentViewController* _vc2;
@@ -18,10 +18,12 @@
18 18
 	RNNNavigationOptions* _options;
19 19
 	RNNTestRootViewCreator* _creator;
20 20
 	RNNEventEmitter* _eventEmitter;
21
+	id _presenter;
21 22
 }
22 23
 
23 24
 - (void)setUp {
24 25
     [super setUp];
26
+	_presenter = [OCMockObject partialMockForObject:[[RNNStackPresenter alloc] init]];
25 27
 	_eventEmitter = [OCMockObject niceMockForClass:[RNNEventEmitter class]];
26 28
 	_creator = [[RNNTestRootViewCreator alloc] init];
27 29
 	_vc1 = [[RNNComponentViewController alloc] initWithLayoutInfo:[RNNLayoutInfo new] rootViewCreator:nil eventEmitter:nil presenter:[OCMockObject partialMockForObject:[[RNNComponentPresenter alloc] init]] options:[[RNNNavigationOptions alloc] initEmptyOptions] defaultOptions:[[RNNNavigationOptions alloc] initEmptyOptions]];
@@ -29,7 +31,7 @@
29 31
 	_vc2Mock = [OCMockObject partialMockForObject:_vc2];
30 32
 	_vc3 = [UIViewController new];
31 33
 	_options = [OCMockObject partialMockForObject:[[RNNNavigationOptions alloc] initEmptyOptions]];
32
-	self.uut = [[RNNStackController alloc] initWithLayoutInfo:nil creator:_creator options:_options defaultOptions:nil presenter:[OCMockObject partialMockForObject:[[RNNStackPresenter alloc] init]] eventEmitter:_eventEmitter childViewControllers:@[_vc1, _vc2]];
34
+	self.uut = [[RNNStackController alloc] initWithLayoutInfo:nil creator:_creator options:_options defaultOptions:nil presenter:_presenter eventEmitter:_eventEmitter childViewControllers:@[_vc1, _vc2]];
33 35
 }
34 36
 
35 37
 - (void)testInitWithLayoutInfo_shouldBindPresenter {
@@ -167,6 +169,22 @@
167 169
 	[(id)self.uut.options verify];
168 170
 }
169 171
 
172
+- (void)testMergeChildOptionsShouldUpdatePresenterForVisibleChild {
173
+	RNNNavigationOptions* options = [[RNNNavigationOptions alloc] initEmptyOptions];
174
+	
175
+	[[_presenter expect] mergeOptions:options resolvedOptions:[OCMArg any]];
176
+	[self.uut mergeChildOptions:options child:self.uut.childViewControllers.lastObject];
177
+	[_presenter verify];
178
+}
179
+
180
+- (void)testMergeChildOptionsShouldNotUpdatePresenterForInvisibleChild {
181
+	RNNNavigationOptions* options = [[RNNNavigationOptions alloc] initEmptyOptions];
182
+	
183
+	[[_presenter reject] mergeOptions:options resolvedOptions:self.uut.resolveOptions];
184
+	[self.uut mergeChildOptions:options child:self.uut.childViewControllers.firstObject];
185
+	[_presenter verify];
186
+}
187
+
170 188
 - (RNNStackController *)createNavigationControllerWithOptions:(RNNNavigationOptions *)options {
171 189
 	RNNStackController* nav = [[RNNStackController alloc] initWithLayoutInfo:nil creator:_creator options:options defaultOptions:nil presenter:[[RNNStackPresenter alloc] init] eventEmitter:nil childViewControllers:@[_vc1]];
172 190
 	return nav;

+ 5
- 3
playground/ios/NavigationTests/RNNTabBarControllerTest.m View File

@@ -7,6 +7,7 @@
7 7
 
8 8
 @interface RNNTabBarControllerTest : XCTestCase
9 9
 
10
+@property(nonatomic, strong) RNNBottomTabsController * originalUut;
10 11
 @property(nonatomic, strong) RNNBottomTabsController * uut;
11 12
 @property(nonatomic, strong) id mockChildViewController;
12 13
 @property(nonatomic, strong) id mockEventEmitter;
@@ -25,7 +26,8 @@
25 26
     self.mockTabBarPresenter = [OCMockObject partialMockForObject:[[RNNBottomTabsPresenter alloc] init]];
26 27
     self.mockChildViewController = [OCMockObject partialMockForObject:[RNNComponentViewController new]];
27 28
     self.mockEventEmitter = [OCMockObject partialMockForObject:[RNNEventEmitter new]];
28
-    self.uut = [OCMockObject partialMockForObject:[[RNNBottomTabsController alloc] initWithLayoutInfo:nil creator:nil options:[[RNNNavigationOptions alloc] initWithDict:@{}] defaultOptions:nil presenter:self.mockTabBarPresenter eventEmitter:self.mockEventEmitter childViewControllers:@[[[UIViewController alloc] init]]]];
29
+	self.originalUut = [[RNNBottomTabsController alloc] initWithLayoutInfo:nil creator:nil options:[[RNNNavigationOptions alloc] initWithDict:@{}] defaultOptions:nil presenter:self.mockTabBarPresenter eventEmitter:self.mockEventEmitter childViewControllers:@[[[UIViewController alloc] init]]];
30
+    self.uut = [OCMockObject partialMockForObject:self.originalUut];
29 31
     OCMStub([self.uut selectedViewController]).andReturn(self.mockChildViewController);
30 32
 }
31 33
 
@@ -104,11 +106,11 @@
104 106
 }
105 107
 
106 108
 - (void)testMergeOptions_shouldInvokeParentMergeOptions {
107
-    id parentMock = [OCMockObject partialMockForObject:[RNNComponentViewController new]];
109
+    id parentMock = [OCMockObject niceMockForClass:[RNNComponentViewController class]];
108 110
     RNNNavigationOptions *options = [[RNNNavigationOptions alloc] initWithDict:@{}];
109 111
 
110 112
     OCMStub([self.uut parentViewController]).andReturn(parentMock);
111
-    [((RNNComponentViewController *) [parentMock expect]) mergeChildOptions:options];
113
+    [((RNNComponentViewController *) [parentMock expect]) mergeChildOptions:options child:self.originalUut];
112 114
     [self.uut mergeOptions:options];
113 115
     [parentMock verify];
114 116
 }

+ 4
- 3
playground/ios/NavigationTests/UIViewController+LayoutProtocolTest.m View File

@@ -63,10 +63,11 @@
63 63
 
64 64
 - (void)testMergeOptions_invokedOnParentViewController {
65 65
     id parent = [OCMockObject partialMockForObject:[RNNStackController new]];
66
+	RNNStackController* uut = [[RNNStackController alloc] initWithLayoutInfo:nil creator:nil options:nil defaultOptions:nil presenter:nil eventEmitter:nil childViewControllers:nil];
67
+	
66 68
     RNNNavigationOptions * toMerge = [[RNNNavigationOptions alloc] initEmptyOptions];
67
-    [(UIViewController *) [parent expect] mergeChildOptions:toMerge];
68
-
69
-    RNNStackController* uut = [[RNNStackController alloc] initWithLayoutInfo:nil creator:nil options:nil defaultOptions:nil presenter:nil eventEmitter:nil childViewControllers:nil];
69
+	[(UIViewController *) [parent expect] mergeChildOptions:toMerge child:uut];
70
+    
70 71
     [parent addChildViewController:uut];
71 72
 
72 73
     [uut mergeOptions:toMerge];

+ 4
- 4
playground/ios/playground.xcodeproj/project.pbxproj View File

@@ -39,7 +39,7 @@
39 39
 		E58D26592385888C003F36BA /* RNNNavigationStackManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D263C2385888C003F36BA /* RNNNavigationStackManagerTest.m */; };
40 40
 		E58D265A2385888C003F36BA /* RNNTestRootViewCreator.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D263D2385888C003F36BA /* RNNTestRootViewCreator.m */; };
41 41
 		E58D265B2385888C003F36BA /* UIViewController+RNNOptionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D263E2385888C003F36BA /* UIViewController+RNNOptionsTest.m */; };
42
-		E58D265C2385888C003F36BA /* RNNNavigationControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D263F2385888C003F36BA /* RNNNavigationControllerTest.m */; };
42
+		E58D265C2385888C003F36BA /* RNNStackControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D263F2385888C003F36BA /* RNNStackControllerTest.m */; };
43 43
 		E58D265D2385888C003F36BA /* RNNControllerFactoryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D26402385888C003F36BA /* RNNControllerFactoryTest.m */; };
44 44
 		E58D265E2385888C003F36BA /* RNNCommandsHandlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D26412385888C003F36BA /* RNNCommandsHandlerTest.m */; };
45 45
 		E58D265F2385888C003F36BA /* RNNBasePresenterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E58D26422385888C003F36BA /* RNNBasePresenterTest.m */; };
@@ -119,7 +119,7 @@
119 119
 		E58D263C2385888C003F36BA /* RNNNavigationStackManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNNavigationStackManagerTest.m; sourceTree = "<group>"; };
120 120
 		E58D263D2385888C003F36BA /* RNNTestRootViewCreator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNTestRootViewCreator.m; sourceTree = "<group>"; };
121 121
 		E58D263E2385888C003F36BA /* UIViewController+RNNOptionsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+RNNOptionsTest.m"; sourceTree = "<group>"; };
122
-		E58D263F2385888C003F36BA /* RNNNavigationControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNNavigationControllerTest.m; sourceTree = "<group>"; };
122
+		E58D263F2385888C003F36BA /* RNNStackControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNStackControllerTest.m; sourceTree = "<group>"; };
123 123
 		E58D26402385888C003F36BA /* RNNControllerFactoryTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNControllerFactoryTest.m; sourceTree = "<group>"; };
124 124
 		E58D26412385888C003F36BA /* RNNCommandsHandlerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNCommandsHandlerTest.m; sourceTree = "<group>"; };
125 125
 		E58D26422385888C003F36BA /* RNNBasePresenterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNBasePresenterTest.m; sourceTree = "<group>"; };
@@ -253,7 +253,7 @@
253 253
 				E58D26282385888B003F36BA /* RNNFontAttributesCreatorTest.m */,
254 254
 				E58D262A2385888B003F36BA /* RNNLayoutManagerTest.m */,
255 255
 				E58D26432385888C003F36BA /* RNNModalManagerTest.m */,
256
-				E58D263F2385888C003F36BA /* RNNNavigationControllerTest.m */,
256
+				E58D263F2385888C003F36BA /* RNNStackControllerTest.m */,
257 257
 				E58D26322385888B003F36BA /* RNNNavigationOptionsTest.m */,
258 258
 				E58D263C2385888C003F36BA /* RNNNavigationStackManagerTest.m */,
259 259
 				E58D262C2385888B003F36BA /* RNNOptionsTest.h */,
@@ -755,7 +755,7 @@
755 755
 				E58D264E2385888C003F36BA /* RNNTestBase.m in Sources */,
756 756
 				E58D26612385888C003F36BA /* RNNTestNoColor.m in Sources */,
757 757
 				E58D265D2385888C003F36BA /* RNNControllerFactoryTest.m in Sources */,
758
-				E58D265C2385888C003F36BA /* RNNNavigationControllerTest.m in Sources */,
758
+				E58D265C2385888C003F36BA /* RNNStackControllerTest.m in Sources */,
759 759
 				E58D26482385888C003F36BA /* RNNDotIndicatorPresenterTest.m in Sources */,
760 760
 				E58D26522385888C003F36BA /* RNNComponentPresenterTest.m in Sources */,
761 761
 				E58D26572385888C003F36BA /* RNNTabBarPresenterTest.m in Sources */,

+ 8
- 1
playground/src/screens/OptionsScreen.js View File

@@ -65,7 +65,14 @@ class Options extends Component {
65 65
     }
66 66
   });
67 67
 
68
-  push = () => Navigation.push(this, Screens.Pushed);
68
+  push = () => Navigation.push(this, {
69
+    component: {
70
+      name: Screens.Pushed,
71
+      passProps: {
72
+        previousScreenIds: [this.props.componentId]
73
+      }
74
+    }
75
+  });
69 76
 
70 77
   hideTopBarInDefaultOptions = () => {
71 78
     Navigation.setDefaultOptions({

+ 9
- 1
playground/src/screens/PushedScreen.js View File

@@ -15,7 +15,8 @@ const {
15 15
   ADD_BACK_HANDLER,
16 16
   REMOVE_BACK_HANDLER,
17 17
   SET_STACK_ROOT_BUTTON,
18
-  PUSH_OPTIONS_BUTTON
18
+  PUSH_OPTIONS_BUTTON,
19
+  HIDE_PREVIOUS_SCREEN_TOP_BAR
19 20
 } = require('../testIDs');
20 21
 const Screens = require('./Screens');
21 22
 
@@ -58,6 +59,7 @@ class PushedScreen extends React.Component {
58 59
         <Button label='Remove BackHandler' testID={REMOVE_BACK_HANDLER} onPress={this.removeBackHandler} />
59 60
         <Button label='Set Stack Root' testID={SET_STACK_ROOT_BUTTON} onPress={this.setStackRoot} />
60 61
         <Button label='Push Options Screen' testID={PUSH_OPTIONS_BUTTON} onPress={this.pushOptionsScreen} />
62
+        <Button label='Hide previous screen top bar' testID={HIDE_PREVIOUS_SCREEN_TOP_BAR} onPress={this.hidePreviousScreenTopBar} />
61 63
       </Root>
62 64
     );
63 65
   }
@@ -101,6 +103,12 @@ class PushedScreen extends React.Component {
101 103
 
102 104
   popToRoot = () => Navigation.popToRoot(this);
103 105
 
106
+  hidePreviousScreenTopBar = () => Navigation.mergeOptions(this.props.previousScreenIds[this.getStackPosition() - 1], {
107
+    topBar: {
108
+      visible: false
109
+    }
110
+  });
111
+
104 112
   setStackRoot = () => Navigation.setStackRoot(this, [
105 113
     {
106 114
       component: {

+ 1
- 0
playground/src/testIDs.js View File

@@ -135,6 +135,7 @@ module.exports = {
135 135
   SHOW_LIFECYCLE_BTN: 'SHOW_LIFECYCLE_BTN',
136 136
   CHANGE_BUTTON_PROPS: 'CHANGE_BUTTON_PROPS',
137 137
   GOTO_BUTTONS_SCREEN: 'GOTO_BUTTONS_SCREEN',
138
+  HIDE_PREVIOUS_SCREEN_TOP_BAR: 'HIDE_PREVIOUS_SCREEN_TOP_BAR',
138 139
 
139 140
   // Elements
140 141
   SCROLLVIEW_ELEMENT: `SCROLLVIEW_ELEMENT`,