Browse Source

Fix setStackRoot crash when called with the same id (#5154)

When a Stack’s root was set with an id of one of the Stack’s current children, there was a crash
since the wrong elements were removed from the stack.

This commit fixes this by creating a new stack when setStackRoot is called, and destroying all ViewControllers from the previous Stack.

Fixes #5117
Guy Carmeli 5 years ago
parent
commit
3c08b1c995
No account linked to committer's email address

+ 5
- 0
e2e/Stack.test.js View File

@@ -80,6 +80,11 @@ describe('Stack', () => {
80 80
     await expect(elementById(TestIDs.STACK_SCREEN_HEADER)).toBeVisible();
81 81
   });
82 82
 
83
+  it('does not crash when setting the stack root to an existing component id', async () => {
84
+    await elementById(TestIDs.SET_STACK_ROOT_WITH_ID_BTN).tap();
85
+    await elementById(TestIDs.SET_STACK_ROOT_WITH_ID_BTN).tap();
86
+  });
87
+
83 88
   it(':ios: set stack root component should be first in stack', async () => {
84 89
     await elementById(TestIDs.PUSH_BTN).tap();
85 90
     await expect(elementByLabel('Stack Position: 1')).toBeVisible();

+ 2
- 8
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/IdStack.java View File

@@ -29,17 +29,11 @@ public class IdStack<E> implements Iterable<String> {
29 29
     }
30 30
 
31 31
 	public E peek() {
32
-		if (isEmpty()) {
33
-			return null;
34
-		}
35
-		return map.get(last(deque));
32
+        return isEmpty() ? null : map.get(last(deque));
36 33
 	}
37 34
 
38 35
 	public E pop() {
39
-		if (isEmpty()) {
40
-			return null;
41
-		}
42
-		return map.remove(removeLast(deque));
36
+	    return isEmpty() ? null : map.remove(removeLast(deque));
43 37
 	}
44 38
 
45 39
 	public boolean isEmpty() {

+ 12
- 15
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java View File

@@ -14,7 +14,6 @@ import com.reactnativenavigation.parse.Options;
14 14
 import com.reactnativenavigation.presentation.Presenter;
15 15
 import com.reactnativenavigation.presentation.StackPresenter;
16 16
 import com.reactnativenavigation.react.Constants;
17
-import com.reactnativenavigation.utils.CollectionUtils;
18 17
 import com.reactnativenavigation.utils.CommandListener;
19 18
 import com.reactnativenavigation.utils.CommandListenerAdapter;
20 19
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
@@ -32,10 +31,11 @@ import java.util.Iterator;
32 31
 import java.util.List;
33 32
 
34 33
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
34
+import static com.reactnativenavigation.utils.CollectionUtils.*;
35 35
 
36 36
 public class StackController extends ParentController<StackLayout> {
37 37
 
38
-    private final IdStack<ViewController> stack = new IdStack<>();
38
+    private IdStack<ViewController> stack = new IdStack<>();
39 39
     private final NavigationAnimator animator;
40 40
     private TopBarController topBarController;
41 41
     private BackButtonHelper backButtonHelper;
@@ -181,20 +181,22 @@ public class StackController extends ParentController<StackLayout> {
181 181
 
182 182
     public void setRoot(List<ViewController> children, CommandListener listener) {
183 183
         animator.cancelPushAnimations();
184
+        IdStack stackToDestroy = stack;
185
+        stack = new IdStack<>();
184 186
         if (children.size() == 1) {
185
-            backButtonHelper.clear(CollectionUtils.last(children));
186
-            push(CollectionUtils.last(children), new CommandListenerAdapter() {
187
+            backButtonHelper.clear(last(children));
188
+            push(last(children), new CommandListenerAdapter() {
187 189
                 @Override
188 190
                 public void onSuccess(String childId) {
189
-                    removeChildrenBellowTop();
191
+                    destroyStack(stackToDestroy);
190 192
                     listener.onSuccess(childId);
191 193
                 }
192 194
             });
193 195
         } else {
194
-            push(CollectionUtils.last(children), new CommandListenerAdapter() {
196
+            push(last(children), new CommandListenerAdapter() {
195 197
                 @Override
196 198
                 public void onSuccess(String childId) {
197
-                    removeChildrenBellowTop();
199
+                    destroyStack(stackToDestroy);
198 200
                     for (int i = 0; i < children.size() - 1; i++) {
199 201
                         stack.set(children.get(i).getId(), children.get(i), i);
200 202
                         children.get(i).setParentController(StackController.this);
@@ -210,14 +212,9 @@ public class StackController extends ParentController<StackLayout> {
210 212
         }
211 213
     }
212 214
 
213
-    private void removeChildrenBellowTop() {
214
-        Iterator<String> iterator = stack.iterator();
215
-        while (stack.size() > 1) {
216
-            ViewController controller = stack.get(iterator.next());
217
-            if (!stack.isTop(controller.getId())) {
218
-                stack.remove(iterator, controller.getId());
219
-                controller.destroy();
220
-            }
215
+    private void destroyStack(IdStack stack) {
216
+        for (String s : (Iterable<String>) stack) {
217
+            ((ViewController) stack.get(s)).destroy();
221 218
         }
222 219
     }
223 220
 

+ 24
- 3
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java View File

@@ -42,7 +42,11 @@ import org.assertj.core.api.iterable.Extractor;
42 42
 import org.json.JSONException;
43 43
 import org.json.JSONObject;
44 44
 import org.junit.Test;
45
-import org.mockito.*;
45
+import org.mockito.ArgumentCaptor;
46
+import org.mockito.InOrder;
47
+import org.mockito.Mockito;
48
+import org.robolectric.Robolectric;
49
+import org.robolectric.shadows.ShadowLooper;
46 50
 
47 51
 import java.util.ArrayList;
48 52
 import java.util.Arrays;
@@ -65,6 +69,7 @@ public class StackControllerTest extends BaseTest {
65 69
     private ChildControllersRegistry childRegistry;
66 70
     private StackController uut;
67 71
     private ViewController child1;
72
+    private ViewController child1a;
68 73
     private ViewController child2;
69 74
     private ViewController child3;
70 75
     private ViewController child4;
@@ -84,6 +89,7 @@ public class StackControllerTest extends BaseTest {
84 89
         renderChecker = spy(new RenderChecker());
85 90
         presenter = spy(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TopBarButtonCreatorMock(), ImageLoaderMock.mock(), renderChecker, new Options()));
86 91
         child1 = spy(new SimpleViewController(activity, childRegistry, "child1", new Options()));
92
+        child1a = spy(new SimpleViewController(activity, childRegistry, "child1", new Options()));
87 93
         child2 = spy(new SimpleViewController(activity, childRegistry, "child2", new Options()));
88 94
         child3 = spy(new SimpleViewController(activity, childRegistry, "child3", new Options()));
89 95
         child4 = spy(new SimpleViewController(activity, childRegistry, "child4", new Options()));
@@ -257,6 +263,8 @@ public class StackControllerTest extends BaseTest {
257 263
 
258 264
     @Test
259 265
     public void setRoot_multipleChildren() {
266
+        Robolectric.getForegroundThreadScheduler().pause();
267
+
260 268
         activity.setContentView(uut.getView());
261 269
         disablePushAnimation(child1, child2, child3, child4);
262 270
         disablePopAnimation(child4);
@@ -264,6 +272,7 @@ public class StackControllerTest extends BaseTest {
264 272
         assertThat(uut.isEmpty()).isTrue();
265 273
         uut.push(child1, new CommandListenerAdapter());
266 274
         uut.push(child2, new CommandListenerAdapter());
275
+        ShadowLooper.idleMainLooper();
267 276
         assertThat(uut.getTopBar().getTitleBar().getNavigationIcon()).isNotNull();
268 277
         uut.setRoot(Arrays.asList(child3, child4), new CommandListenerAdapter() {
269 278
             @Override
@@ -275,6 +284,7 @@ public class StackControllerTest extends BaseTest {
275 284
 
276 285
                 assertThat(uut.getCurrentChild()).isEqualTo(child4);
277 286
                 uut.pop(Options.EMPTY, new CommandListenerAdapter());
287
+                ShadowLooper.idleMainLooper();
278 288
                 assertThat(uut.getTopBar().getTitleBar().getNavigationIcon()).isNull();
279 289
                 assertThat(uut.getCurrentChild()).isEqualTo(child3);
280 290
             }
@@ -286,14 +296,25 @@ public class StackControllerTest extends BaseTest {
286 296
         disablePushAnimation(child1);
287 297
         uut.setRoot(Collections.singletonList(child1), new CommandListenerAdapter());
288 298
 
299
+        ViewGroup c2View = child2.getView();
300
+        ViewGroup c3View = child3.getView();
289 301
         uut.setRoot(Collections.singletonList(child2), new CommandListenerAdapter());
290 302
         uut.setRoot(Collections.singletonList(child3), new CommandListenerAdapter());
291
-        animator.endPushAnimation(child2.getView());
292
-        animator.endPushAnimation(child3.getView());
303
+        animator.endPushAnimation(c2View);
304
+        animator.endPushAnimation(c3View);
293 305
 
294 306
         assertContainsOnlyId(child3.getId());
295 307
     }
296 308
 
309
+    @Test
310
+    public void setRoot_doesNotCrashWhenCalledWithSameId() {
311
+        disablePushAnimation(child1, child1a);
312
+        uut.setRoot(Collections.singletonList(child1), new CommandListenerAdapter());
313
+        uut.setRoot(Collections.singletonList(child1a), new CommandListenerAdapter());
314
+
315
+        assertContainsOnlyId(child1a.getId());
316
+    }
317
+
297 318
     @Test
298 319
     public synchronized void pop() {
299 320
         disablePushAnimation(child1, child2);

+ 13
- 2
playground/src/screens/StackScreen.js View File

@@ -3,7 +3,7 @@ const Root = require('../components/Root');
3 3
 const Button = require('../components/Button');
4 4
 const Screens = require('./Screens');
5 5
 const Navigation = require('../services/Navigation');
6
-const {stack, component} = require('../commons/Layouts');
6
+const { stack, component } = require('../commons/Layouts');
7 7
 const {
8 8
   PUSH_BTN,
9 9
   STACK_SCREEN_HEADER,
@@ -12,7 +12,8 @@ const {
12 12
   PUSH_CUSTOM_BACK_BTN,
13 13
   CUSTOM_BACK_BTN,
14 14
   SEARCH_BTN,
15
-  SET_STACK_ROOT_BTN
15
+  SET_STACK_ROOT_BTN,
16
+  SET_STACK_ROOT_WITH_ID_BTN
16 17
 } = require('../testIDs');
17 18
 
18 19
 class StackScreen extends React.Component {
@@ -39,6 +40,7 @@ class StackScreen extends React.Component {
39 40
         <Button label='Pop None Existent Screen' testID={POP_NONE_EXISTENT_SCREEN_BTN} onPress={this.popNoneExistent} />
40 41
         <Button label='Push Custom Back Button' testID={PUSH_CUSTOM_BACK_BTN} onPress={this.pushCustomBackButton} />
41 42
         <Button label='Set Stack Root' testID={SET_STACK_ROOT_BTN} onPress={this.setStackRoot} />
43
+        <Button label='Set Stack Root With ID' testID={SET_STACK_ROOT_WITH_ID_BTN} onPress={this.setStackRootWithId} />
42 44
         <Button label='Search' testID={SEARCH_BTN} onPress={this.search} />
43 45
       </Root>
44 46
     );
@@ -73,6 +75,15 @@ class StackScreen extends React.Component {
73 75
     component(Screens.Pushed, { topBar: { title: { text: 'Screen A' } } }),
74 76
     component(Screens.Pushed, { topBar: { title: { text: 'Screen B' } } }),
75 77
   ]));
78
+
79
+  setStackRootWithId = () => Navigation.setStackRoot(this,
80
+    {
81
+      component: {
82
+        id: 'StackRootWithId',
83
+        name: Screens.Stack
84
+      }
85
+    },
86
+  );
76 87
 }
77 88
 
78 89
 module.exports = StackScreen;

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

@@ -83,6 +83,7 @@ module.exports = {
83 83
   DISMISS_BTN: 'DISMISS_BTN',
84 84
   SEARCH_BTN: 'SEARCH_BTN',
85 85
   SET_STACK_ROOT_BTN: 'SET_STACK_ROOT_BTN',
86
+  SET_STACK_ROOT_WITH_ID_BTN: 'SET_STACK_ROOT_WITH_ID_BTN',
86 87
 
87 88
   // Buttons
88 89
   TAB_BASED_APP_BUTTON: `TAB_BASED_APP_BUTTON`,

+ 1
- 1
scripts/test-e2e.js View File

@@ -19,5 +19,5 @@ function run() {
19 19
     if (!skipBuild) {
20 20
         exec.execSync(`detox build --configuration ${configuration}`);
21 21
     }
22
-    exec.execSync(`detox test --configuration ${configuration} ${headless$} ${!android ? `-w ${workers}` : ``} --loglevel trace`); //-f "ScreenStyle.test.js" --loglevel trace
22
+    exec.execSync(`detox test --configuration ${configuration} ${headless$} ${!android ? `-w ${workers}` : ``}`); //-f "ScreenStyle.test.js" --loglevel trace
23 23
 }