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
     await expect(elementById(TestIDs.STACK_SCREEN_HEADER)).toBeVisible();
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
   it(':ios: set stack root component should be first in stack', async () => {
88
   it(':ios: set stack root component should be first in stack', async () => {
84
     await elementById(TestIDs.PUSH_BTN).tap();
89
     await elementById(TestIDs.PUSH_BTN).tap();
85
     await expect(elementByLabel('Stack Position: 1')).toBeVisible();
90
     await expect(elementByLabel('Stack Position: 1')).toBeVisible();

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

29
     }
29
     }
30
 
30
 
31
 	public E peek() {
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
 	public E pop() {
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
 	public boolean isEmpty() {
39
 	public boolean isEmpty() {

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

14
 import com.reactnativenavigation.presentation.Presenter;
14
 import com.reactnativenavigation.presentation.Presenter;
15
 import com.reactnativenavigation.presentation.StackPresenter;
15
 import com.reactnativenavigation.presentation.StackPresenter;
16
 import com.reactnativenavigation.react.Constants;
16
 import com.reactnativenavigation.react.Constants;
17
-import com.reactnativenavigation.utils.CollectionUtils;
18
 import com.reactnativenavigation.utils.CommandListener;
17
 import com.reactnativenavigation.utils.CommandListener;
19
 import com.reactnativenavigation.utils.CommandListenerAdapter;
18
 import com.reactnativenavigation.utils.CommandListenerAdapter;
20
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
19
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
32
 import java.util.List;
31
 import java.util.List;
33
 
32
 
34
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
33
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
34
+import static com.reactnativenavigation.utils.CollectionUtils.*;
35
 
35
 
36
 public class StackController extends ParentController<StackLayout> {
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
     private final NavigationAnimator animator;
39
     private final NavigationAnimator animator;
40
     private TopBarController topBarController;
40
     private TopBarController topBarController;
41
     private BackButtonHelper backButtonHelper;
41
     private BackButtonHelper backButtonHelper;
181
 
181
 
182
     public void setRoot(List<ViewController> children, CommandListener listener) {
182
     public void setRoot(List<ViewController> children, CommandListener listener) {
183
         animator.cancelPushAnimations();
183
         animator.cancelPushAnimations();
184
+        IdStack stackToDestroy = stack;
185
+        stack = new IdStack<>();
184
         if (children.size() == 1) {
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
                 @Override
189
                 @Override
188
                 public void onSuccess(String childId) {
190
                 public void onSuccess(String childId) {
189
-                    removeChildrenBellowTop();
191
+                    destroyStack(stackToDestroy);
190
                     listener.onSuccess(childId);
192
                     listener.onSuccess(childId);
191
                 }
193
                 }
192
             });
194
             });
193
         } else {
195
         } else {
194
-            push(CollectionUtils.last(children), new CommandListenerAdapter() {
196
+            push(last(children), new CommandListenerAdapter() {
195
                 @Override
197
                 @Override
196
                 public void onSuccess(String childId) {
198
                 public void onSuccess(String childId) {
197
-                    removeChildrenBellowTop();
199
+                    destroyStack(stackToDestroy);
198
                     for (int i = 0; i < children.size() - 1; i++) {
200
                     for (int i = 0; i < children.size() - 1; i++) {
199
                         stack.set(children.get(i).getId(), children.get(i), i);
201
                         stack.set(children.get(i).getId(), children.get(i), i);
200
                         children.get(i).setParentController(StackController.this);
202
                         children.get(i).setParentController(StackController.this);
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
 import org.json.JSONException;
42
 import org.json.JSONException;
43
 import org.json.JSONObject;
43
 import org.json.JSONObject;
44
 import org.junit.Test;
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
 import java.util.ArrayList;
51
 import java.util.ArrayList;
48
 import java.util.Arrays;
52
 import java.util.Arrays;
65
     private ChildControllersRegistry childRegistry;
69
     private ChildControllersRegistry childRegistry;
66
     private StackController uut;
70
     private StackController uut;
67
     private ViewController child1;
71
     private ViewController child1;
72
+    private ViewController child1a;
68
     private ViewController child2;
73
     private ViewController child2;
69
     private ViewController child3;
74
     private ViewController child3;
70
     private ViewController child4;
75
     private ViewController child4;
84
         renderChecker = spy(new RenderChecker());
89
         renderChecker = spy(new RenderChecker());
85
         presenter = spy(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TopBarButtonCreatorMock(), ImageLoaderMock.mock(), renderChecker, new Options()));
90
         presenter = spy(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TopBarButtonCreatorMock(), ImageLoaderMock.mock(), renderChecker, new Options()));
86
         child1 = spy(new SimpleViewController(activity, childRegistry, "child1", new Options()));
91
         child1 = spy(new SimpleViewController(activity, childRegistry, "child1", new Options()));
92
+        child1a = spy(new SimpleViewController(activity, childRegistry, "child1", new Options()));
87
         child2 = spy(new SimpleViewController(activity, childRegistry, "child2", new Options()));
93
         child2 = spy(new SimpleViewController(activity, childRegistry, "child2", new Options()));
88
         child3 = spy(new SimpleViewController(activity, childRegistry, "child3", new Options()));
94
         child3 = spy(new SimpleViewController(activity, childRegistry, "child3", new Options()));
89
         child4 = spy(new SimpleViewController(activity, childRegistry, "child4", new Options()));
95
         child4 = spy(new SimpleViewController(activity, childRegistry, "child4", new Options()));
257
 
263
 
258
     @Test
264
     @Test
259
     public void setRoot_multipleChildren() {
265
     public void setRoot_multipleChildren() {
266
+        Robolectric.getForegroundThreadScheduler().pause();
267
+
260
         activity.setContentView(uut.getView());
268
         activity.setContentView(uut.getView());
261
         disablePushAnimation(child1, child2, child3, child4);
269
         disablePushAnimation(child1, child2, child3, child4);
262
         disablePopAnimation(child4);
270
         disablePopAnimation(child4);
264
         assertThat(uut.isEmpty()).isTrue();
272
         assertThat(uut.isEmpty()).isTrue();
265
         uut.push(child1, new CommandListenerAdapter());
273
         uut.push(child1, new CommandListenerAdapter());
266
         uut.push(child2, new CommandListenerAdapter());
274
         uut.push(child2, new CommandListenerAdapter());
275
+        ShadowLooper.idleMainLooper();
267
         assertThat(uut.getTopBar().getTitleBar().getNavigationIcon()).isNotNull();
276
         assertThat(uut.getTopBar().getTitleBar().getNavigationIcon()).isNotNull();
268
         uut.setRoot(Arrays.asList(child3, child4), new CommandListenerAdapter() {
277
         uut.setRoot(Arrays.asList(child3, child4), new CommandListenerAdapter() {
269
             @Override
278
             @Override
275
 
284
 
276
                 assertThat(uut.getCurrentChild()).isEqualTo(child4);
285
                 assertThat(uut.getCurrentChild()).isEqualTo(child4);
277
                 uut.pop(Options.EMPTY, new CommandListenerAdapter());
286
                 uut.pop(Options.EMPTY, new CommandListenerAdapter());
287
+                ShadowLooper.idleMainLooper();
278
                 assertThat(uut.getTopBar().getTitleBar().getNavigationIcon()).isNull();
288
                 assertThat(uut.getTopBar().getTitleBar().getNavigationIcon()).isNull();
279
                 assertThat(uut.getCurrentChild()).isEqualTo(child3);
289
                 assertThat(uut.getCurrentChild()).isEqualTo(child3);
280
             }
290
             }
286
         disablePushAnimation(child1);
296
         disablePushAnimation(child1);
287
         uut.setRoot(Collections.singletonList(child1), new CommandListenerAdapter());
297
         uut.setRoot(Collections.singletonList(child1), new CommandListenerAdapter());
288
 
298
 
299
+        ViewGroup c2View = child2.getView();
300
+        ViewGroup c3View = child3.getView();
289
         uut.setRoot(Collections.singletonList(child2), new CommandListenerAdapter());
301
         uut.setRoot(Collections.singletonList(child2), new CommandListenerAdapter());
290
         uut.setRoot(Collections.singletonList(child3), new CommandListenerAdapter());
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
         assertContainsOnlyId(child3.getId());
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
     @Test
318
     @Test
298
     public synchronized void pop() {
319
     public synchronized void pop() {
299
         disablePushAnimation(child1, child2);
320
         disablePushAnimation(child1, child2);

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

3
 const Button = require('../components/Button');
3
 const Button = require('../components/Button');
4
 const Screens = require('./Screens');
4
 const Screens = require('./Screens');
5
 const Navigation = require('../services/Navigation');
5
 const Navigation = require('../services/Navigation');
6
-const {stack, component} = require('../commons/Layouts');
6
+const { stack, component } = require('../commons/Layouts');
7
 const {
7
 const {
8
   PUSH_BTN,
8
   PUSH_BTN,
9
   STACK_SCREEN_HEADER,
9
   STACK_SCREEN_HEADER,
12
   PUSH_CUSTOM_BACK_BTN,
12
   PUSH_CUSTOM_BACK_BTN,
13
   CUSTOM_BACK_BTN,
13
   CUSTOM_BACK_BTN,
14
   SEARCH_BTN,
14
   SEARCH_BTN,
15
-  SET_STACK_ROOT_BTN
15
+  SET_STACK_ROOT_BTN,
16
+  SET_STACK_ROOT_WITH_ID_BTN
16
 } = require('../testIDs');
17
 } = require('../testIDs');
17
 
18
 
18
 class StackScreen extends React.Component {
19
 class StackScreen extends React.Component {
39
         <Button label='Pop None Existent Screen' testID={POP_NONE_EXISTENT_SCREEN_BTN} onPress={this.popNoneExistent} />
40
         <Button label='Pop None Existent Screen' testID={POP_NONE_EXISTENT_SCREEN_BTN} onPress={this.popNoneExistent} />
40
         <Button label='Push Custom Back Button' testID={PUSH_CUSTOM_BACK_BTN} onPress={this.pushCustomBackButton} />
41
         <Button label='Push Custom Back Button' testID={PUSH_CUSTOM_BACK_BTN} onPress={this.pushCustomBackButton} />
41
         <Button label='Set Stack Root' testID={SET_STACK_ROOT_BTN} onPress={this.setStackRoot} />
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
         <Button label='Search' testID={SEARCH_BTN} onPress={this.search} />
44
         <Button label='Search' testID={SEARCH_BTN} onPress={this.search} />
43
       </Root>
45
       </Root>
44
     );
46
     );
73
     component(Screens.Pushed, { topBar: { title: { text: 'Screen A' } } }),
75
     component(Screens.Pushed, { topBar: { title: { text: 'Screen A' } } }),
74
     component(Screens.Pushed, { topBar: { title: { text: 'Screen B' } } }),
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
 module.exports = StackScreen;
89
 module.exports = StackScreen;

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

83
   DISMISS_BTN: 'DISMISS_BTN',
83
   DISMISS_BTN: 'DISMISS_BTN',
84
   SEARCH_BTN: 'SEARCH_BTN',
84
   SEARCH_BTN: 'SEARCH_BTN',
85
   SET_STACK_ROOT_BTN: 'SET_STACK_ROOT_BTN',
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
   // Buttons
88
   // Buttons
88
   TAB_BASED_APP_BUTTON: `TAB_BASED_APP_BUTTON`,
89
   TAB_BASED_APP_BUTTON: `TAB_BASED_APP_BUTTON`,

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

19
     if (!skipBuild) {
19
     if (!skipBuild) {
20
         exec.execSync(`detox build --configuration ${configuration}`);
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
 }