Browse Source

Modal as layout (#3094)

[Breaking[ Modal refactor

* [Breaking] Showing modal removes the previously visible ViewController from hierarchy
* Modal implementation does not rely on Dialog
* Modal view is added to `content` layout
* Overlay is added to `root` layout, on top of the application's content
* Introduce StackControllerBuilder
* Show modal with animation
* Dismiss modal with animation
* Add previous modal at index 0
* Add root at index 0
* dismissAllModals dismisses only top modal with animation
Guy Carmeli 6 years ago
parent
commit
728f87e2ed
No account linked to committer's email address
33 changed files with 994 additions and 183 deletions
  1. 1
    1
      e2e/ScreenStack.test.js
  2. 2
    1
      lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java
  3. 2
    3
      lib/android/app/src/main/java/com/reactnativenavigation/anim/BaseAnimator.java
  4. 0
    1
      lib/android/app/src/main/java/com/reactnativenavigation/anim/ModalAnimator.java
  5. 34
    0
      lib/android/app/src/main/java/com/reactnativenavigation/anim/ModalAnimator2.java
  6. 13
    4
      lib/android/app/src/main/java/com/reactnativenavigation/parse/AnimationOptions.java
  7. 9
    8
      lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java
  8. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java
  9. 4
    9
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ModalStack.java
  10. 58
    22
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java
  11. 4
    8
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java
  12. 64
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackControllerBuilder.java
  13. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java
  14. 3
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java
  15. 2
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/Modal.java
  16. 63
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java
  17. 111
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalStack2.java
  18. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java
  19. 16
    0
      lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java
  20. 14
    13
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java
  21. 8
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ComponentViewControllerTest.java
  22. 8
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/FloatingActionButtonTest.java
  23. 11
    14
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ModalStackTest.java
  24. 74
    22
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java
  25. 16
    2
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java
  26. 8
    9
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ParentControllerTest.java
  27. 39
    47
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/StackControllerTest.java
  28. 8
    7
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarButtonControllerTest.java
  29. 8
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java
  30. 9
    2
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ViewControllerTest.java
  31. 37
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalAnimatorMock.java
  32. 142
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java
  33. 223
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest2.java

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

@@ -16,7 +16,7 @@ describe('screen stack', () => {
16 16
     await expect(elementById(testIDs.WELCOME_SCREEN_HEADER)).toBeVisible();
17 17
   });
18 18
 
19
-  it(':android: push and pop screen with out animation', async () => {
19
+  it(':android: push and pop screen without animation', async () => {
20 20
     await elementById(testIDs.PUSH_OPTIONS_BUTTON).tap();
21 21
     await expect(elementById(testIDs.OPTIONS_SCREEN_HEADER)).toBeVisible();
22 22
     Android.pressBack();

+ 2
- 1
lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java View File

@@ -6,6 +6,7 @@ import android.support.v7.app.AppCompatActivity;
6 6
 import android.view.KeyEvent;
7 7
 
8 8
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
9
+import com.reactnativenavigation.utils.CommandListenerAdapter;
9 10
 import com.reactnativenavigation.viewcontrollers.Navigator;
10 11
 
11 12
 public class NavigationActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
@@ -40,7 +41,7 @@ public class NavigationActivity extends AppCompatActivity implements DefaultHard
40 41
 
41 42
 	@Override
42 43
 	public void invokeDefaultOnBackPressed() {
43
-		if (!navigator.handleBack()) {
44
+		if (!navigator.handleBack(new CommandListenerAdapter())) {
44 45
 			super.onBackPressed();
45 46
 		}
46 47
 	}

+ 2
- 3
lib/android/app/src/main/java/com/reactnativenavigation/anim/BaseAnimator.java View File

@@ -33,9 +33,8 @@ class BaseAnimator {
33 33
 
34 34
         AnimatorSet set = new AnimatorSet();
35 35
         ObjectAnimator translationY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, this.translationY, 0);
36
-        translationY.setInterpolator(DECELERATE_INTERPOLATOR);
37
-        translationY.setDuration(DURATION);
38
-        alpha.setDuration(DURATION);
36
+        set.setInterpolator(DECELERATE_INTERPOLATOR);
37
+        set.setDuration(DURATION);
39 38
         set.playTogether(translationY, alpha);
40 39
         return set;
41 40
     }

+ 0
- 1
lib/android/app/src/main/java/com/reactnativenavigation/anim/ModalAnimator.java View File

@@ -6,7 +6,6 @@ import android.animation.AnimatorSet;
6 6
 import android.content.Context;
7 7
 import android.view.View;
8 8
 
9
-import com.reactnativenavigation.parse.AnimationOptions;
10 9
 import com.reactnativenavigation.parse.AnimationsOptions;
11 10
 
12 11
 public class ModalAnimator extends BaseAnimator {

+ 34
- 0
lib/android/app/src/main/java/com/reactnativenavigation/anim/ModalAnimator2.java View File

@@ -0,0 +1,34 @@
1
+package com.reactnativenavigation.anim;
2
+
3
+
4
+import android.animation.Animator;
5
+import android.animation.AnimatorListenerAdapter;
6
+import android.content.Context;
7
+import android.view.View;
8
+
9
+import com.reactnativenavigation.parse.AnimationOptions;
10
+
11
+public class ModalAnimator2 extends BaseAnimator {
12
+
13
+    private Animator animator;
14
+
15
+    public ModalAnimator2(Context context) {
16
+        super(context);
17
+    }
18
+
19
+    public void show(View view, AnimationOptions animation, AnimatorListenerAdapter listener) {
20
+        animator = animation.getAnimation(view, getDefaultPushAnimation(view));
21
+        animator.addListener(listener);
22
+        animator.start();
23
+    }
24
+
25
+    public void dismiss(View view, AnimatorListenerAdapter listener) {
26
+        animator = options.dismissModal.getAnimation(view, getDefaultPopAnimation(view));
27
+        animator.addListener(listener);
28
+        animator.start();
29
+    }
30
+
31
+    public boolean isRunning() {
32
+        return animator != null && animator.isRunning();
33
+    }
34
+}

+ 13
- 4
lib/android/app/src/main/java/com/reactnativenavigation/parse/AnimationOptions.java View File

@@ -6,8 +6,11 @@ import android.animation.AnimatorSet;
6 6
 import android.util.Property;
7 7
 import android.view.View;
8 8
 
9
+import com.reactnativenavigation.parse.params.Bool;
10
+import com.reactnativenavigation.parse.params.NullBool;
9 11
 import com.reactnativenavigation.parse.params.NullText;
10 12
 import com.reactnativenavigation.parse.params.Text;
13
+import com.reactnativenavigation.parse.parsers.BoolParser;
11 14
 import com.reactnativenavigation.parse.parsers.TextParser;
12 15
 
13 16
 import org.json.JSONObject;
@@ -26,10 +29,15 @@ public class AnimationOptions {
26 29
         options.hasValue = true;
27 30
         for (Iterator<String> it = json.keys(); it.hasNext(); ) {
28 31
             String key = it.next();
29
-            if ("id".equals(key)) {
30
-                options.id = TextParser.parse(json, key);
31
-            } else {
32
-                options.valueOptions.add(ValueAnimationOptions.parse(json.optJSONObject(key), getAnimProp(key)));
32
+            switch (key) {
33
+                case "id":
34
+                    options.id = TextParser.parse(json, key);
35
+                    break;
36
+                case "enable":
37
+                    options.enable = BoolParser.parse(json, key);
38
+                    break;
39
+                default:
40
+                    options.valueOptions.add(ValueAnimationOptions.parse(json.optJSONObject(key), getAnimProp(key)));
33 41
             }
34 42
         }
35 43
 
@@ -39,6 +47,7 @@ public class AnimationOptions {
39 47
     private boolean hasValue = false;
40 48
 
41 49
     public Text id = new NullText();
50
+    public Bool enable = new NullBool();
42 51
     private HashSet<ValueAnimationOptions> valueOptions = new HashSet<>();
43 52
 
44 53
     void mergeWith(AnimationOptions other) {

+ 9
- 8
lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java View File

@@ -9,6 +9,7 @@ import com.reactnativenavigation.utils.TypefaceLoader;
9 9
 import com.reactnativenavigation.viewcontrollers.ComponentViewController;
10 10
 import com.reactnativenavigation.viewcontrollers.SideMenuController;
11 11
 import com.reactnativenavigation.viewcontrollers.StackController;
12
+import com.reactnativenavigation.viewcontrollers.StackControllerBuilder;
12 13
 import com.reactnativenavigation.viewcontrollers.ViewController;
13 14
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
14 15
 import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentCreator;
@@ -123,14 +124,14 @@ public class LayoutFactory {
123 124
     }
124 125
 
125 126
 	private ViewController createStack(LayoutNode node) {
126
-        StackController stackController = new StackController(activity,
127
-                new TitleBarButtonCreator(reactInstanceManager),
128
-                new TitleBarReactViewCreator(reactInstanceManager),
129
-                new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreator(reactInstanceManager)),
130
-                new TopBarController(),
131
-                node.id,
132
-                getOptions(node)
133
-        );
127
+        StackController stackController = new StackControllerBuilder(activity)
128
+                .setTopBarButtonCreator(new TitleBarButtonCreator(reactInstanceManager))
129
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreator(reactInstanceManager))
130
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreator(reactInstanceManager)))
131
+                .setTopBarController(new TopBarController())
132
+                .setId(node.id)
133
+                .setInitialOptions(getOptions(node))
134
+                .createStackController();
134 135
         addChildrenToStack(node.children, stackController);
135 136
         return stackController;
136 137
 	}

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationModule.java View File

@@ -98,7 +98,7 @@ public class NavigationModule extends ReactContextBaseJavaModule {
98 98
 		final LayoutNode layoutTree = LayoutNodeParser.parse(new JSONObject(rawLayoutTree.toHashMap()));
99 99
 		handle(() -> {
100 100
             final ViewController viewController = newLayoutFactory().create(layoutTree);
101
-            navigator().showModal(viewController, promise);
101
+            navigator().showModal(viewController, new CommandListenerAdapter(promise));
102 102
         });
103 103
 	}
104 104
 

+ 4
- 9
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ModalStack.java View File

@@ -2,7 +2,6 @@ package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.support.annotation.Nullable;
4 4
 
5
-import com.facebook.react.bridge.Promise;
6 5
 import com.reactnativenavigation.utils.CommandListenerAdapter;
7 6
 import com.reactnativenavigation.utils.Task;
8 7
 import com.reactnativenavigation.viewcontrollers.Navigator.CommandListener;
@@ -24,21 +23,17 @@ class ModalStack implements ModalListener {
24 23
         this.modalListener = modalListener;
25 24
     }
26 25
 
27
-    void showModal(final ViewController viewController, Promise promise) {
26
+    void showModal(final ViewController viewController, CommandListener listener) {
28 27
         Modal modal = creator.create(viewController, this);
29 28
         modals.add(modal);
30 29
         modal.show();
31
-        promise.resolve(viewController.getId());
30
+        listener.onSuccess(viewController.getId());
32 31
     }
33 32
 
34 33
     void dismissModal(final String componentId, CommandListener listener) {
35 34
         applyOnModal(componentId, (modal) -> modal.dismiss(listener), () -> listener.onError("Nothing to dismiss"));
36 35
     }
37 36
 
38
-    void dismissAll() {
39
-        dismissAll(new CommandListenerAdapter());
40
-    }
41
-
42 37
     void dismissAll(CommandListener listener) {
43 38
         for (Modal modal : modals) {
44 39
             modal.dismiss(size() == 1 ? listener : new CommandListenerAdapter());
@@ -90,8 +85,8 @@ class ModalStack implements ModalListener {
90 85
         return isEmpty() ? null : modals.get(modals.size() - 1);
91 86
     }
92 87
 
93
-    public boolean handleBack() {
94
-        return !modals.isEmpty() && peek().handleBack();
88
+    public boolean handleBack(CommandListener listener) {
89
+        return !modals.isEmpty() && peek().handleBack(listener);
95 90
     }
96 91
 
97 92
     private void applyOnModal(String componentId, Task<Modal> accept, Runnable reject) {

+ 58
- 22
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java View File

@@ -5,20 +5,21 @@ import android.animation.AnimatorListenerAdapter;
5 5
 import android.app.Activity;
6 6
 import android.support.annotation.NonNull;
7 7
 import android.support.annotation.Nullable;
8
-import android.view.View;
9 8
 import android.view.ViewGroup;
10 9
 import android.widget.FrameLayout;
11 10
 
12 11
 import com.facebook.react.bridge.Promise;
12
+import com.reactnativenavigation.anim.ModalAnimator2;
13 13
 import com.reactnativenavigation.anim.NavigationAnimator;
14
-import com.reactnativenavigation.parse.AnimationsOptions;
15 14
 import com.reactnativenavigation.parse.Options;
16 15
 import com.reactnativenavigation.presentation.NavigationOptionsListener;
17 16
 import com.reactnativenavigation.presentation.OverlayManager;
17
+import com.reactnativenavigation.utils.CommandListenerAdapter;
18 18
 import com.reactnativenavigation.utils.CompatUtils;
19 19
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
20
-import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
21 20
 import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
21
+import com.reactnativenavigation.viewcontrollers.modal.ModalPresenter;
22
+import com.reactnativenavigation.viewcontrollers.modal.ModalStack2;
22 23
 
23 24
 import java.util.Collection;
24 25
 import java.util.Collections;
@@ -31,20 +32,32 @@ public class Navigator extends ParentController implements ModalListener {
31 32
         void onError(String message);
32 33
     }
33 34
 
34
-    private final ModalStack modalStack;
35
+//    private final ModalStack modalStack;
36
+    private final ModalStack2 modalStack;
35 37
     private ViewController root;
38
+    private FrameLayout rootLayout;
39
+    private FrameLayout contentLayout;
36 40
     private OverlayManager overlayManager = new OverlayManager();
37 41
     private Options defaultOptions = new Options();
38 42
 
39 43
     public Navigator(final Activity activity) {
40 44
         super(activity, "navigator" + CompatUtils.generateViewId(), new Options());
41
-        modalStack = new ModalStack(new ModalCreator(), this);
45
+//        modalStack = new ModalStack(new ModalCreator(), this);
46
+        modalStack = new ModalStack2(new ModalPresenter(new ModalAnimator2(activity)));
47
+    }
48
+
49
+    public FrameLayout getContentLayout() {
50
+        return contentLayout;
42 51
     }
43 52
 
44 53
     @NonNull
45 54
     @Override
46 55
     protected ViewGroup createView() {
47
-        return new FrameLayout(getActivity());
56
+        rootLayout = new FrameLayout(getActivity());
57
+        contentLayout = new FrameLayout(getActivity());
58
+        rootLayout.addView(contentLayout);
59
+        modalStack.setContentLayout(contentLayout);
60
+        return rootLayout;
48 61
     }
49 62
 
50 63
     @NonNull
@@ -54,13 +67,16 @@ public class Navigator extends ParentController implements ModalListener {
54 67
     }
55 68
 
56 69
     @Override
57
-    public boolean handleBack() {
58
-        return modalStack.isEmpty() ? root.handleBack() : modalStack.handleBack();
70
+    public boolean handleBack(CommandListener listener) {
71
+        if (modalStack.isEmpty()) return root.handleBack(listener);
72
+        return modalStack.handleBack(listener, () -> {
73
+            if (modalStack.size() == 1) contentLayout.addView(root.getView(), 0);
74
+        });
59 75
     }
60 76
 
61 77
     @Override
62 78
     public void destroy() {
63
-        modalStack.dismissAll();
79
+        modalStack.dismissAllModals(new CommandListenerAdapter());
64 80
         super.destroy();
65 81
     }
66 82
 
@@ -75,13 +91,11 @@ public class Navigator extends ParentController implements ModalListener {
75 91
         }
76 92
 
77 93
         root = viewController;
78
-        View view = viewController.getView();
79 94
 
80
-        AnimationsOptions animationsOptions = viewController.options.animations;
81
-        getView().addView(view);
82
-        if (animationsOptions.startApp.hasValue()) {
83
-            new NavigationAnimator(viewController.getActivity(), animationsOptions)
84
-                    .animateStartApp(view, new AnimatorListenerAdapter() {
95
+        contentLayout.addView(viewController.getView());
96
+        if (viewController.options.animations.startApp.hasValue()) {
97
+            new NavigationAnimator(viewController.getActivity(), viewController.options.animations)
98
+                    .animateStartApp(viewController.getView(), new AnimatorListenerAdapter() {
85 99
                         @Override
86 100
                         public void onAnimationEnd(Animator animation) {
87 101
                             promise.resolve(viewController.getId());
@@ -126,7 +140,7 @@ public class Navigator extends ParentController implements ModalListener {
126 140
         }
127 141
     }
128 142
 
129
-    void pop(final String fromId, CommandListener listener) {
143
+    public void pop(final String fromId, CommandListener listener) {
130 144
         ViewController from = findControllerById(fromId);
131 145
         if (from != null) {
132 146
             from.performOnParentStack(stack -> ((StackController) stack).pop(listener));
@@ -158,12 +172,34 @@ public class Navigator extends ParentController implements ModalListener {
158 172
         }
159 173
     }
160 174
 
161
-    public void showModal(final ViewController viewController, Promise promise) {
162
-        modalStack.showModal(viewController, promise);
175
+    public void showModal(final ViewController viewController, CommandListener listener) {
176
+        modalStack.showModal(viewController, new CommandListenerAdapter() {
177
+            @Override
178
+            public void onSuccess(String childId) {
179
+                contentLayout.removeView(root.getView());
180
+                listener.onSuccess(childId);
181
+            }
182
+
183
+            @Override
184
+            public void onError(String message) {
185
+                listener.onError(message);
186
+            }
187
+        });
163 188
     }
164 189
 
165 190
     public void dismissModal(final String componentId, CommandListener listener) {
166
-        modalStack.dismissModal(componentId, listener);
191
+        modalStack.dismissModal(componentId, new CommandListener() {
192
+            @Override
193
+            public void onSuccess(String childId) {
194
+                if (modalStack.size() == 0) contentLayout.addView(root.getView());
195
+                listener.onSuccess(childId);
196
+            }
197
+
198
+            @Override
199
+            public void onError(String message) {
200
+                listener.onError(message);
201
+            }
202
+        });
167 203
     }
168 204
 
169 205
     @Override
@@ -173,7 +209,6 @@ public class Navigator extends ParentController implements ModalListener {
173 209
         }
174 210
     }
175 211
 
176
-
177 212
     @Override
178 213
     public void onModalDismiss(Modal modal) {
179 214
         if (modalStack.isEmpty()) {
@@ -182,11 +217,12 @@ public class Navigator extends ParentController implements ModalListener {
182 217
     }
183 218
 
184 219
     public void dismissAllModals(CommandListener listener) {
185
-        modalStack.dismissAll(listener);
220
+        if (!modalStack.isEmpty()) contentLayout.addView(root.getView(), 0);
221
+        modalStack.dismissAllModals(listener);
186 222
     }
187 223
 
188 224
     public void showOverlay(ViewController overlay) {
189
-        overlayManager.show(getView(), overlay);
225
+        overlayManager.show(rootLayout, overlay);
190 226
     }
191 227
 
192 228
     public void dismissOverlay(final String componentId) {

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

@@ -31,13 +31,13 @@ public class StackController extends ParentController<StackLayout> {
31 31
     private TopBarBackgroundViewController topBarBackgroundViewController;
32 32
     private TopBarController topBarController;
33 33
 
34
-    public StackController(final Activity activity, ReactViewCreator topBarButtonCreator, TitleBarReactViewCreator titleBarReactViewCreator, TopBarBackgroundViewController topBarBackgroundViewController, TopBarController topBarController, String id, Options initialOptions) {
34
+    public StackController(final Activity activity, ReactViewCreator topBarButtonCreator, TitleBarReactViewCreator titleBarReactViewCreator, TopBarBackgroundViewController topBarBackgroundViewController, TopBarController topBarController, NavigationAnimator animator, String id, Options initialOptions) {
35 35
         super(activity, id, initialOptions);
36 36
         this.topBarController = topBarController;
37
-        animator = createAnimator();
38 37
         this.topBarButtonCreator = topBarButtonCreator;
39 38
         this.titleBarReactViewCreator = titleBarReactViewCreator;
40 39
         this.topBarBackgroundViewController = topBarBackgroundViewController;
40
+        this.animator = animator;
41 41
     }
42 42
 
43 43
     public void applyOptions(Options options) {
@@ -220,9 +220,9 @@ public class StackController extends ParentController<StackLayout> {
220 220
     }
221 221
 
222 222
     @Override
223
-    public boolean handleBack() {
223
+    public boolean handleBack(CommandListener listener) {
224 224
         if (canPop()) {
225
-            pop(new CommandListenerAdapter());
225
+            pop(listener);
226 226
             return true;
227 227
         }
228 228
         return false;
@@ -259,10 +259,6 @@ public class StackController extends ParentController<StackLayout> {
259 259
         topBarController.clearTopTabs();
260 260
     }
261 261
 
262
-     NavigationAnimator createAnimator() {
263
-        return new NavigationAnimator(getActivity());
264
-    }
265
-
266 262
     @RestrictTo(RestrictTo.Scope.TESTS)
267 263
     TopBar getTopBar() {
268 264
         return topBarController.getView();

+ 64
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackControllerBuilder.java View File

@@ -0,0 +1,64 @@
1
+package com.reactnativenavigation.viewcontrollers;
2
+
3
+import android.app.Activity;
4
+
5
+import com.reactnativenavigation.anim.NavigationAnimator;
6
+import com.reactnativenavigation.parse.Options;
7
+import com.reactnativenavigation.viewcontrollers.topbar.TopBarBackgroundViewController;
8
+import com.reactnativenavigation.viewcontrollers.topbar.TopBarController;
9
+import com.reactnativenavigation.views.titlebar.TitleBarReactViewCreator;
10
+
11
+public class StackControllerBuilder {
12
+    private Activity activity;
13
+    private ReactViewCreator topBarButtonCreator;
14
+    private TitleBarReactViewCreator titleBarReactViewCreator;
15
+    private TopBarBackgroundViewController topBarBackgroundViewController;
16
+    private TopBarController topBarController;
17
+    private String id;
18
+    private Options initialOptions = new Options();
19
+    private NavigationAnimator animator;
20
+
21
+    public StackControllerBuilder(Activity activity) {
22
+        this.activity = activity;
23
+        animator = new NavigationAnimator(activity);
24
+    }
25
+
26
+    public StackControllerBuilder setTopBarButtonCreator(ReactViewCreator topBarButtonCreator) {
27
+        this.topBarButtonCreator = topBarButtonCreator;
28
+        return this;
29
+    }
30
+
31
+    public StackControllerBuilder setTitleBarReactViewCreator(TitleBarReactViewCreator titleBarReactViewCreator) {
32
+        this.titleBarReactViewCreator = titleBarReactViewCreator;
33
+        return this;
34
+    }
35
+
36
+    public StackControllerBuilder setTopBarBackgroundViewController(TopBarBackgroundViewController topBarBackgroundViewController) {
37
+        this.topBarBackgroundViewController = topBarBackgroundViewController;
38
+        return this;
39
+    }
40
+
41
+    public StackControllerBuilder setTopBarController(TopBarController topBarController) {
42
+        this.topBarController = topBarController;
43
+        return this;
44
+    }
45
+
46
+    public StackControllerBuilder setId(String id) {
47
+        this.id = id;
48
+        return this;
49
+    }
50
+
51
+    public StackControllerBuilder setInitialOptions(Options initialOptions) {
52
+        this.initialOptions = initialOptions;
53
+        return this;
54
+    }
55
+
56
+    public StackControllerBuilder setAnimator(NavigationAnimator animator) {
57
+        this.animator = animator;
58
+        return this;
59
+    }
60
+
61
+    public StackController createStackController() {
62
+        return new StackController(activity, topBarButtonCreator, titleBarReactViewCreator, topBarBackgroundViewController, topBarController, animator, id, initialOptions);
63
+    }
64
+}

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java View File

@@ -62,7 +62,7 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
62 62
         getView();
63 63
     }
64 64
 
65
-    public boolean handleBack() {
65
+    public boolean handleBack(Navigator.CommandListener listener) {
66 66
         return false;
67 67
     }
68 68
 

+ 3
- 2
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java View File

@@ -13,6 +13,7 @@ import com.reactnativenavigation.parse.Options;
13 13
 import com.reactnativenavigation.presentation.BottomTabsOptionsPresenter;
14 14
 import com.reactnativenavigation.presentation.NavigationOptionsListener;
15 15
 import com.reactnativenavigation.utils.ImageLoader;
16
+import com.reactnativenavigation.viewcontrollers.Navigator.CommandListener;
16 17
 import com.reactnativenavigation.viewcontrollers.ParentController;
17 18
 import com.reactnativenavigation.viewcontrollers.ViewController;
18 19
 import com.reactnativenavigation.views.BottomTabs;
@@ -76,8 +77,8 @@ public class BottomTabsController extends ParentController implements AHBottomNa
76 77
     }
77 78
 
78 79
     @Override
79
-	public boolean handleBack() {
80
-		return !tabs.isEmpty() && tabs.get(bottomTabs.getCurrentItem()).handleBack();
80
+	public boolean handleBack(CommandListener listener) {
81
+		return !tabs.isEmpty() && tabs.get(bottomTabs.getCurrentItem()).handleBack(listener);
81 82
 	}
82 83
 
83 84
     @Override

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

@@ -89,8 +89,8 @@ public class Modal implements DialogInterface.OnKeyListener, DialogInterface.OnD
89 89
         modalListener.onModalDisplay(this);
90 90
     }
91 91
 
92
-    public boolean handleBack() {
93
-        if (!viewController.handleBack()) {
92
+    public boolean handleBack(CommandListener listener) {
93
+        if (!viewController.handleBack(listener)) {
94 94
             dialog.dismiss();
95 95
         }
96 96
         return true;

+ 63
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java View File

@@ -0,0 +1,63 @@
1
+package com.reactnativenavigation.viewcontrollers.modal;
2
+
3
+import android.animation.Animator;
4
+import android.animation.AnimatorListenerAdapter;
5
+import android.view.ViewGroup;
6
+
7
+import com.reactnativenavigation.anim.ModalAnimator2;
8
+import com.reactnativenavigation.viewcontrollers.Navigator.CommandListener;
9
+import com.reactnativenavigation.viewcontrollers.ViewController;
10
+
11
+import javax.annotation.Nullable;
12
+
13
+public class ModalPresenter {
14
+
15
+    private ViewGroup content;
16
+    private ModalAnimator2 animator;
17
+
18
+    public ModalPresenter(ModalAnimator2 animator) {
19
+        this.animator = animator;
20
+    }
21
+
22
+    public void setContentLayout(ViewGroup contentLayout) {
23
+        this.content = contentLayout;
24
+    }
25
+
26
+    public void showModal(ViewController toAdd, @Nullable ViewController toRemove, CommandListener listener) {
27
+        content.addView(toAdd.getView());
28
+        if (toAdd.options.animations.showModal.enable.isTrueOrUndefined()) {
29
+            animator.show(toAdd.getView(), toAdd.options.animations.showModal, new AnimatorListenerAdapter() {
30
+                @Override
31
+                public void onAnimationEnd(Animator animation) {
32
+                    onShowModalEnd(toRemove, listener, toAdd);
33
+                }
34
+            });
35
+        } else {
36
+            onShowModalEnd(toRemove, listener, toAdd);
37
+        }
38
+    }
39
+
40
+    private void onShowModalEnd(@Nullable ViewController toRemove, CommandListener listener, ViewController toAdd) {
41
+        if (toRemove != null) content.removeView(toRemove.getView());
42
+        listener.onSuccess(toAdd.getId());
43
+    }
44
+
45
+    public void dismissModal(ViewController toDismiss, @Nullable ViewController toAdd, CommandListener listener) {
46
+        if (toAdd != null) content.addView(toAdd.getView(), 0);
47
+        if (toDismiss.options.animations.dismissModal.enable.isTrueOrUndefined()) {
48
+            animator.dismiss(toDismiss.getView(), new AnimatorListenerAdapter() {
49
+                @Override
50
+                public void onAnimationEnd(Animator animation) {
51
+                    onDismissEnd(toDismiss, listener);
52
+                }
53
+            });
54
+        } else {
55
+            onDismissEnd(toDismiss, listener);
56
+        }
57
+    }
58
+
59
+    private void onDismissEnd(ViewController toDismiss, CommandListener listener) {
60
+        toDismiss.destroy();
61
+        listener.onSuccess(toDismiss.getId());
62
+    }
63
+}

+ 111
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalStack2.java View File

@@ -0,0 +1,111 @@
1
+package com.reactnativenavigation.viewcontrollers.modal;
2
+
3
+import android.view.ViewGroup;
4
+
5
+import com.reactnativenavigation.viewcontrollers.Navigator.CommandListener;
6
+import com.reactnativenavigation.viewcontrollers.ViewController;
7
+
8
+import java.util.ArrayList;
9
+import java.util.EmptyStackException;
10
+import java.util.List;
11
+
12
+import javax.annotation.Nullable;
13
+
14
+public class ModalStack2 {
15
+    private List<ViewController> modals = new ArrayList<>();
16
+    private final ModalPresenter presenter;
17
+
18
+    public ModalStack2(ModalPresenter presenter) {
19
+        this.presenter = presenter;
20
+    }
21
+
22
+    public void setContentLayout(ViewGroup contentLayout) {
23
+        presenter.setContentLayout(contentLayout);
24
+    }
25
+
26
+    public void showModal(ViewController viewController, CommandListener listener) {
27
+        ViewController toRemove = isEmpty() ? null : peek();
28
+        modals.add(viewController);
29
+        presenter.showModal(viewController, toRemove, listener);
30
+    }
31
+
32
+    public void dismissModal(String componentId, CommandListener listener) {
33
+        ViewController toDismiss = findModalByComponentId(componentId);
34
+        if (toDismiss != null) {
35
+            ViewController toAdd = isTop(toDismiss) ? get(size() - 2) : null;
36
+            modals.remove(toDismiss);
37
+            presenter.dismissModal(toDismiss, toAdd, listener);
38
+        } else {
39
+            listener.onError("Nothing to dismiss");
40
+        }
41
+    }
42
+
43
+    public void dismissAllModals(CommandListener listener) {
44
+        if (modals.isEmpty()) {
45
+            listener.onError("Nothing to dismiss");
46
+            return;
47
+        }
48
+
49
+        while (!modals.isEmpty()) {
50
+            if (modals.size() == 1) {
51
+                dismissModal(modals.get(0).getId(), listener);
52
+            } else {
53
+                modals.get(0).destroy();
54
+                modals.remove(0);
55
+            }
56
+        }
57
+    }
58
+
59
+    public boolean handleBack(CommandListener listener, Runnable onModalWillDismiss) {
60
+        if (isEmpty()) return false;
61
+        if (peek().handleBack(listener)) {
62
+            return true;
63
+        }
64
+        onModalWillDismiss.run();
65
+        dismissModal(peek().getId(), listener);
66
+        return true;
67
+    }
68
+
69
+    public ViewController peek() {
70
+        if (modals.isEmpty()) throw new EmptyStackException();
71
+        return modals.get(modals.size() - 1);
72
+    }
73
+
74
+    public ViewController get(int index) {
75
+        return modals.get(index);
76
+    }
77
+
78
+    public boolean isEmpty() {
79
+        return modals.isEmpty();
80
+    }
81
+
82
+    public int size() {
83
+        return modals.size();
84
+    }
85
+
86
+    private boolean isTop(ViewController modal) {
87
+        return size() > 1 && peek().equals(modal);
88
+    }
89
+
90
+    @Nullable
91
+    private ViewController findModalByComponentId(String componentId) {
92
+        for (ViewController modal : modals) {
93
+            if (modal.findControllerById(componentId) != null) {
94
+                return modal;
95
+            }
96
+        }
97
+        return null;
98
+    }
99
+
100
+
101
+    @Nullable
102
+    public ViewController findControllerById(String componentId) {
103
+        for (ViewController modal : modals) {
104
+            ViewController controllerById = modal.findControllerById(componentId);
105
+            if (controllerById != null) {
106
+                return controllerById;
107
+            }
108
+        }
109
+        return null;
110
+    }
111
+}

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java View File

@@ -160,7 +160,7 @@ public class TitleBar extends Toolbar {
160 160
             button.destroy();
161 161
         }
162 162
         rightButtonControllers.clear();
163
-        getMenu().clear();
163
+        if (getMenu().size() > 0) getMenu().clear();
164 164
     }
165 165
 
166 166
     public void setLeftButtons(List<Button> leftButtons) {

+ 16
- 0
lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java View File

@@ -50,7 +50,23 @@ public abstract class BaseTest {
50 50
         }
51 51
     }
52 52
 
53
+    protected void disableShowModalAnimation(ViewController... modals) {
54
+        for (ViewController modal : modals) {
55
+            modal.options.animations.showModal.enable = new Bool(false);
56
+        }
57
+    }
58
+
59
+    protected void disableDismissModalAnimation(ViewController... modals) {
60
+        for (ViewController modal : modals) {
61
+            modal.options.animations.dismissModal.enable = new Bool(false);
62
+        }
63
+    }
64
+
53 65
     protected void dispatchPreDraw(View view) {
54 66
         view.getViewTreeObserver().dispatchOnPreDraw();
55 67
     }
68
+
69
+    protected void dispatchOnGlobalLayout(View view) {
70
+        view.getViewTreeObserver().dispatchOnGlobalLayout();
71
+    }
56 72
 }

+ 14
- 13
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java View File

@@ -30,6 +30,7 @@ import java.util.Collections;
30 30
 import java.util.List;
31 31
 
32 32
 import static org.assertj.core.api.Java6Assertions.assertThat;
33
+import static org.mockito.ArgumentMatchers.any;
33 34
 import static org.mockito.Mockito.spy;
34 35
 import static org.mockito.Mockito.times;
35 36
 import static org.mockito.Mockito.verify;
@@ -113,19 +114,19 @@ public class BottomTabsControllerTest extends BaseTest {
113 114
 
114 115
     @Test
115 116
     public void handleBack_DelegatesToSelectedChild() {
116
-        assertThat(uut.handleBack()).isFalse();
117
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
117 118
 
118 119
         List<ViewController> tabs = createTabs();
119 120
         ViewController spy = spy(tabs.get(2));
120 121
         tabs.set(2, spy);
121
-        when(spy.handleBack()).thenReturn(true);
122
+        when(spy.handleBack(any())).thenReturn(true);
122 123
         uut.setTabs(tabs);
123 124
 
124
-        assertThat(uut.handleBack()).isFalse();
125
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
125 126
         uut.selectTabAtIndex(2);
126
-        assertThat(uut.handleBack()).isTrue();
127
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isTrue();
127 128
 
128
-        verify(spy, times(1)).handleBack();
129
+        verify(spy, times(1)).handleBack(any());
129 130
     }
130 131
 
131 132
     @Test
@@ -175,13 +176,13 @@ public class BottomTabsControllerTest extends BaseTest {
175 176
     }
176 177
 
177 178
     private StackController createStack(String id) {
178
-        return new StackController(activity,
179
-                new TopBarButtonCreatorMock(),
180
-                new TitleBarReactViewCreatorMock(),
181
-                new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()),
182
-                new TopBarController(),
183
-                id,
184
-                tabOptions
185
-        );
179
+        return new StackControllerBuilder(activity)
180
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
181
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
182
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
183
+                .setTopBarController(new TopBarController())
184
+                .setId(id)
185
+                .setInitialOptions(tabOptions)
186
+                .createStackController();
186 187
     }
187 188
 }

+ 8
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ComponentViewControllerTest.java View File

@@ -30,7 +30,14 @@ public class ComponentViewControllerTest extends BaseTest {
30 30
         super.beforeEach();
31 31
         Activity activity = newActivity();
32 32
         view = spy(new TestComponentLayout(activity, new TestReactView(activity)));
33
-        ParentController<StackLayout> parentController = new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "stack", new Options());
33
+        ParentController<StackLayout> parentController = new StackControllerBuilder(activity)
34
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
35
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
36
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
37
+                .setTopBarController(new TopBarController())
38
+                .setId("stack")
39
+                .setInitialOptions(new Options())
40
+                .createStackController();
34 41
         uut = new ComponentViewController(activity, "componentId1", "componentName", (activity1, componentId, componentName) -> view, new Options());
35 42
         uut.setParentController(parentController);
36 43
         parentController.ensureViewIsCreated();

+ 8
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/FloatingActionButtonTest.java View File

@@ -37,7 +37,14 @@ public class FloatingActionButtonTest extends BaseTest {
37 37
     public void beforeEach() {
38 38
         super.beforeEach();
39 39
         activity = newActivity();
40
-        stackController = new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "stackController", new Options());
40
+        stackController = new StackControllerBuilder(activity)
41
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
42
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
43
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
44
+                .setTopBarController(new TopBarController())
45
+                .setId("stackController")
46
+                .setInitialOptions(new Options())
47
+                .createStackController();
41 48
         Options options = getOptionsWithFab();
42 49
         childFab = new SimpleViewController(activity, "child1", options);
43 50
         childNoFab = new SimpleViewController(activity, "child2", new Options());

+ 11
- 14
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ModalStackTest.java View File

@@ -1,7 +1,6 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import com.reactnativenavigation.BaseTest;
4
-import com.reactnativenavigation.mocks.MockPromise;
5 4
 import com.reactnativenavigation.mocks.ModalCreatorMock;
6 5
 import com.reactnativenavigation.mocks.SimpleViewController;
7 6
 import com.reactnativenavigation.parse.Options;
@@ -11,8 +10,6 @@ import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
11 10
 
12 11
 import org.junit.Test;
13 12
 
14
-import javax.annotation.Nullable;
15
-
16 13
 import static org.assertj.core.api.Java6Assertions.assertThat;
17 14
 import static org.mockito.ArgumentMatchers.any;
18 15
 import static org.mockito.Mockito.spy;
@@ -50,15 +47,15 @@ public class ModalStackTest extends BaseTest {
50 47
 
51 48
     @Test
52 49
     public void modalRefIsSaved() {
53
-        uut.showModal(viewController, new MockPromise());
50
+        uut.showModal(viewController, new CommandListenerAdapter());
54 51
         assertThat(findModal(CONTROLLER_ID)).isNotNull();
55 52
     }
56 53
 
57 54
     @Test
58 55
     public void modalIsShown() {
59
-        uut.showModal(viewController, new MockPromise() {
56
+        uut.showModal(viewController, new CommandListenerAdapter() {
60 57
             @Override
61
-            public void resolve(@Nullable Object value) {
58
+            public void onSuccess(String childId) {
62 59
                 verify(findModal(CONTROLLER_ID), times(1)).show();
63 60
                 verify(modalListener, times(1)).onModalDisplay(any());
64 61
             }
@@ -67,7 +64,7 @@ public class ModalStackTest extends BaseTest {
67 64
 
68 65
     @Test
69 66
     public void modalIsDismissed() {
70
-        uut.showModal(viewController, new MockPromise());
67
+        uut.showModal(viewController, new CommandListenerAdapter());
71 68
         final Modal modal = findModal(CONTROLLER_ID);
72 69
         assertThat(modal).isNotNull();
73 70
         uut.dismissModal(CONTROLLER_ID, new CommandListenerAdapter() {
@@ -81,8 +78,8 @@ public class ModalStackTest extends BaseTest {
81 78
 
82 79
     @Test
83 80
     public void dismissAllModals() {
84
-        uut.showModal(new SimpleViewController(newActivity(), "1", new Options()), new MockPromise());
85
-        uut.showModal(new SimpleViewController(newActivity(), "2", new Options()), new MockPromise());
81
+        uut.showModal(new SimpleViewController(newActivity(), "1", new Options()), new CommandListenerAdapter());
82
+        uut.showModal(new SimpleViewController(newActivity(), "2", new Options()), new CommandListenerAdapter());
86 83
         uut.dismissAll(new CommandListenerAdapter() {
87 84
             @Override
88 85
             public void onSuccess(String childId) {
@@ -94,7 +91,7 @@ public class ModalStackTest extends BaseTest {
94 91
     @Test
95 92
     public void isEmpty() {
96 93
         assertThat(uut.isEmpty()).isTrue();
97
-        uut.showModal(viewController, new MockPromise());
94
+        uut.showModal(viewController, new CommandListenerAdapter());
98 95
         assertThat(uut.isEmpty()).isFalse();
99 96
         uut.dismissAll(new CommandListenerAdapter());
100 97
         assertThat(uut.isEmpty()).isTrue();
@@ -102,8 +99,8 @@ public class ModalStackTest extends BaseTest {
102 99
 
103 100
     @Test
104 101
     public void onDismiss() {
105
-        uut.showModal(viewController, new MockPromise());
106
-        uut.showModal(new SimpleViewController(newActivity(), "otherComponent", new Options()), new MockPromise());
102
+        uut.showModal(viewController, new CommandListenerAdapter());
103
+        uut.showModal(new SimpleViewController(newActivity(), "otherComponent", new Options()), new CommandListenerAdapter());
107 104
         uut.dismissAll(new CommandListenerAdapter() {
108 105
             @Override
109 106
             public void onSuccess(String childId) {
@@ -115,8 +112,8 @@ public class ModalStackTest extends BaseTest {
115 112
     @Test
116 113
     public void onDismiss_onViewAppearedInvokedOnPreviousModal() {
117 114
         SimpleViewController viewController = spy(new SimpleViewController(newActivity(), "otherComponent", new Options()));
118
-        uut.showModal(viewController, new MockPromise());
119
-        uut.showModal(this.viewController, new MockPromise());
115
+        uut.showModal(viewController, new CommandListenerAdapter());
116
+        uut.showModal(this.viewController, new CommandListenerAdapter());
120 117
         uut.dismissModal(CONTROLLER_ID, new CommandListenerAdapter() {
121 118
             @Override
122 119
             public void onSuccess(String childId) {

+ 74
- 22
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java View File

@@ -29,6 +29,7 @@ import java.util.Arrays;
29 29
 import javax.annotation.Nullable;
30 30
 
31 31
 import static org.assertj.core.api.Java6Assertions.assertThat;
32
+import static org.mockito.ArgumentMatchers.any;
32 33
 import static org.mockito.Mockito.spy;
33 34
 import static org.mockito.Mockito.times;
34 35
 import static org.mockito.Mockito.verify;
@@ -59,20 +60,21 @@ public class NavigatorTest extends BaseTest {
59 60
         child3 = new SimpleViewController(activity, "child3", tabOptions);
60 61
         child4 = new SimpleViewController(activity, "child4", tabOptions);
61 62
         child5 = new SimpleViewController(activity, "child5", tabOptions);
63
+        activity.setContentView(uut.getView());
62 64
     }
63 65
 
64 66
     @Test
65 67
     public void setRoot_AddsChildControllerView() {
66
-        assertThat(uut.getView().getChildCount()).isZero();
68
+        assertThat(uut.getContentLayout().getChildCount()).isZero();
67 69
         uut.setRoot(child1, new MockPromise());
68
-        assertIsChild(uut.getView(), child1.getView());
70
+        assertIsChild(uut.getContentLayout(), child1.getView());
69 71
     }
70 72
 
71 73
     @Test
72 74
     public void setRoot_ReplacesExistingChildControllerViews() {
73 75
         uut.setRoot(child1, new MockPromise());
74 76
         uut.setRoot(child2, new MockPromise());
75
-        assertIsChild(uut.getView(), child2.getView());
77
+        assertIsChild(uut.getContentLayout(), child2.getView());
76 78
     }
77 79
 
78 80
     @Test
@@ -229,17 +231,17 @@ public class NavigatorTest extends BaseTest {
229 231
     public void handleBack_DelegatesToRoot() {
230 232
         ViewController root = spy(child1);
231 233
         uut.setRoot(root, new MockPromise());
232
-        when(root.handleBack()).thenReturn(true);
233
-        assertThat(uut.handleBack()).isTrue();
234
-        verify(root, times(1)).handleBack();
234
+        when(root.handleBack(any(Navigator.CommandListener.class))).thenReturn(true);
235
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isTrue();
236
+        verify(root, times(1)).handleBack(any());
235 237
     }
236 238
 
237 239
     @Test
238 240
     public void handleBack_modalTakePrecedenceOverRoot() {
239 241
         ViewController root = spy(child1);
240 242
         uut.setRoot(root, new MockPromise());
241
-        uut.showModal(child2, new MockPromise());
242
-        verify(root, times(0)).handleBack();
243
+        uut.showModal(child2, new CommandListenerAdapter());
244
+        verify(root, times(0)).handleBack(new CommandListenerAdapter());
243 245
     }
244 246
 
245 247
     @Test
@@ -268,8 +270,14 @@ public class NavigatorTest extends BaseTest {
268 270
 
269 271
     @NonNull
270 272
     private StackController newStack() {
271
-        return new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(),
272
-                "stack" + CompatUtils.generateViewId(), tabOptions);
273
+        return new StackControllerBuilder(activity)
274
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
275
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
276
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
277
+                .setTopBarController(new TopBarController())
278
+                .setId("stack" + CompatUtils.generateViewId())
279
+                .setInitialOptions(tabOptions)
280
+                .createStackController();
273 281
     }
274 282
 
275 283
     @Test
@@ -339,7 +347,7 @@ public class NavigatorTest extends BaseTest {
339 347
         uut.setRoot(parentController, new MockPromise());
340 348
         StackController stackController = newStack();
341 349
         stackController.push(child1, new CommandListenerAdapter());
342
-        uut.showModal(stackController, new MockPromise());
350
+        uut.showModal(stackController, new CommandListenerAdapter());
343 351
         uut.push(stackController.getId(), child2, new CommandListenerAdapter());
344 352
         assertIsChild(stackController.getView(), child2.getView());
345 353
     }
@@ -371,28 +379,72 @@ public class NavigatorTest extends BaseTest {
371 379
 
372 380
     @Test
373 381
     public void showModal_onViewDisappearIsInvokedOnRoot() {
374
-        uut.setRoot(parentController, new MockPromise());
375
-        uut.showModal(child1, new MockPromise() {
382
+        uut.setRoot(parentController, new MockPromise() {
376 383
             @Override
377 384
             public void resolve(@Nullable Object value) {
378
-                verify(parentController, times(1)).onViewLostFocus();
385
+                uut.showModal(child1, new CommandListenerAdapter() {
386
+                    @Override
387
+                    public void onSuccess(String childId) {
388
+                        assertThat(parentController.getView().getParent()).isNull();
389
+                        verify(parentController, times(1)).onViewDisappear();
390
+                    }
391
+                });
379 392
             }
380 393
         });
381 394
     }
382 395
 
383 396
     @Test
384 397
     public void dismissModal_onViewAppearedInvokedOnRoot() {
398
+        disableShowModalAnimation(child1, child2);
399
+        disableDismissModalAnimation(child1, child2);
400
+
401
+        uut.setRoot(parentController, new MockPromise());
402
+        uut.showModal(child1, new CommandListenerAdapter());
403
+        uut.showModal(child2, new CommandListenerAdapter());
404
+
405
+        uut.dismissModal(child2.getId(), new CommandListenerAdapter());
406
+        assertThat(parentController.getView().getParent()).isNull();
407
+        verify(parentController, times(1)).onViewAppeared();
408
+
409
+        uut.dismissModal(child1.getId(), new CommandListenerAdapter());
410
+        assertThat(parentController.getView().getParent()).isNotNull();
411
+
412
+        verify(parentController, times(2)).onViewAppeared();
413
+    }
414
+
415
+    @Test
416
+    public void dismissAllModals_onViewAppearedInvokedOnRoot() {
417
+        disableShowModalAnimation(child1);
418
+
419
+        uut.dismissAllModals(new CommandListenerAdapter());
420
+        verify(parentController, times(0)).onViewAppeared();
421
+
385 422
         uut.setRoot(parentController, new MockPromise());
386
-        uut.showModal(child1, new MockPromise() {
423
+        verify(parentController, times(1)).onViewAppeared();
424
+        uut.showModal(child1, new CommandListenerAdapter());
425
+        uut.dismissAllModals(new CommandListenerAdapter());
426
+
427
+        verify(parentController, times(2)).onViewAppeared();
428
+    }
429
+
430
+    @Test
431
+    public void handleBack_onViewAppearedInvokedOnRoot() {
432
+        disableShowModalAnimation(child1, child2);
433
+
434
+        uut.setRoot(parentController, new MockPromise());
435
+        uut.showModal(child1, new CommandListenerAdapter());
436
+        uut.showModal(child2, new CommandListenerAdapter());
437
+
438
+        uut.handleBack(new CommandListenerAdapter());
439
+        verify(parentController, times(1)).onViewAppeared();
440
+
441
+        uut.handleBack(new CommandListenerAdapter() {
387 442
             @Override
388
-            public void resolve(@Nullable Object value) {
389
-                uut.dismissModal("child1", new CommandListenerAdapter() {
390
-                    @Override
391
-                    public void onSuccess(String childId) {
392
-                        verify(parentController, times(1)).onViewRegainedFocus();
393
-                    }
394
-                });
443
+            public void onSuccess(String childId) {
444
+                assertThat(parentController.getView().getParent()).isNotNull();
445
+                verify(parentController, times(2)).onViewAppeared();
395 446
             }
396 447
         });
397 448
     }
449
+
398 450
 }

+ 16
- 2
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java View File

@@ -53,7 +53,14 @@ public class OptionsApplyingTest extends BaseTest {
53 53
                 (activity1, componentId, componentName) -> view,
54 54
                 initialNavigationOptions
55 55
         );
56
-        stackController = new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "stack", new Options());
56
+        stackController = new StackControllerBuilder(activity)
57
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
58
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
59
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
60
+                .setTopBarController(new TopBarController())
61
+                .setId("stack")
62
+                .setInitialOptions(new Options())
63
+                .createStackController();
57 64
         stackController.ensureViewIsCreated();
58 65
         stackController.getView().layout(0, 0, 1000, 1000);
59 66
         stackController.getTopBar().layout(0, 0, 1000, 100);
@@ -73,7 +80,14 @@ public class OptionsApplyingTest extends BaseTest {
73 80
     public void initialOptionsAppliedOnAppear() {
74 81
         uut.options.topBar.title.text = new Text("the title");
75 82
         StackController stackController =
76
-                new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "stackId", new Options());
83
+                new StackControllerBuilder(activity)
84
+                        .setTopBarButtonCreator(new TopBarButtonCreatorMock())
85
+                        .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
86
+                        .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
87
+                        .setTopBarController(new TopBarController())
88
+                        .setId("stackId")
89
+                        .setInitialOptions(new Options())
90
+                        .createStackController();
77 91
         stackController.push(uut, new CommandListenerAdapter());
78 92
         assertThat(stackController.getTopBar().getTitle()).isEmpty();
79 93
 

+ 8
- 9
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ParentControllerTest.java View File

@@ -155,14 +155,13 @@ public class ParentControllerTest extends BaseTest {
155 155
     }
156 156
 
157 157
     private StackController createStack() {
158
-        return new StackController(
159
-                activity,
160
-                new TopBarButtonCreatorMock(),
161
-                new TitleBarReactViewCreatorMock(),
162
-                new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()),
163
-                new TopBarController(),
164
-                "stack",
165
-                new Options()
166
-        );
158
+        return new StackControllerBuilder(activity)
159
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
160
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
161
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
162
+                .setTopBarController(new TopBarController())
163
+                .setId("stack")
164
+                .setInitialOptions(new Options())
165
+                .createStackController();
167 166
     }
168 167
 }

+ 39
- 47
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/StackControllerTest.java View File

@@ -2,7 +2,6 @@ package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.app.Activity;
4 4
 import android.content.Context;
5
-import android.support.annotation.NonNull;
6 5
 import android.view.View;
7 6
 
8 7
 import com.reactnativenavigation.BaseTest;
@@ -53,6 +52,7 @@ public class StackControllerTest extends BaseTest {
53 52
     @Override
54 53
     public void beforeEach() {
55 54
         super.beforeEach();
55
+        animator = Mockito.mock(NavigationAnimator.class);
56 56
         activity = newActivity();
57 57
         uut = createStackController();
58 58
         child1 = spy(new SimpleViewController(activity, "child1", new Options()));
@@ -140,14 +140,14 @@ public class StackControllerTest extends BaseTest {
140 140
     public void pop_layoutHandlesChildWillDisappear() {
141 141
         final StackLayout[] stackLayout = new StackLayout[1];
142 142
         uut =
143
-                new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "uut", new Options()) {
144
-                    @NonNull
145
-                    @Override
146
-                    protected StackLayout createView() {
147
-                        stackLayout[0] = spy(super.createView());
148
-                        return stackLayout[0];
149
-                    }
150
-                };
143
+                new StackControllerBuilder(activity)
144
+                        .setTopBarButtonCreator(new TopBarButtonCreatorMock())
145
+                        .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
146
+                        .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
147
+                        .setTopBarController(new TopBarController())
148
+                        .setId("uut")
149
+                        .setInitialOptions(new Options())
150
+                        .createStackController();
151 151
         uut.push(child1, new CommandListenerAdapter());
152 152
         uut.push(child2, new CommandListenerAdapter() {
153 153
             @Override
@@ -176,20 +176,20 @@ public class StackControllerTest extends BaseTest {
176 176
     @Test
177 177
     public void handleBack_PopsUnlessSingleChild() {
178 178
         assertThat(uut.isEmpty()).isTrue();
179
-        assertThat(uut.handleBack()).isFalse();
179
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
180 180
 
181 181
         uut.push(child1, new CommandListenerAdapter());
182 182
         assertThat(uut.size()).isEqualTo(1);
183
-        assertThat(uut.handleBack()).isFalse();
183
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
184 184
 
185 185
         uut.push(child2, new CommandListenerAdapter() {
186 186
             @Override
187 187
             public void onSuccess(String childId) {
188 188
                 assertThat(uut.size()).isEqualTo(2);
189
-                assertThat(uut.handleBack()).isTrue();
189
+                assertThat(uut.handleBack(new CommandListenerAdapter())).isTrue();
190 190
 
191 191
                 assertThat(uut.size()).isEqualTo(1);
192
-                assertThat(uut.handleBack()).isFalse();
192
+                assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
193 193
             }
194 194
         });
195 195
     }
@@ -636,34 +636,30 @@ public class StackControllerTest extends BaseTest {
636 636
 
637 637
     @Test
638 638
     public void mergeChildOptions_updatesViewWithNewOptions() {
639
-        final StackLayout[] stackLayout = new StackLayout[1];
640
-        StackController uut =
641
-                new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "stack", new Options()) {
642
-                    @NonNull
643
-                    @Override
644
-                    protected StackLayout createView() {
645
-                        stackLayout[0] = spy(super.createView());
646
-                        return stackLayout[0];
647
-                    }
648
-                };
639
+        StackController uut = spy(new StackControllerBuilder(activity)
640
+                        .setTopBarButtonCreator(new TopBarButtonCreatorMock())
641
+                        .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
642
+                        .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
643
+                        .setTopBarController(new TopBarController())
644
+                        .setId("stack")
645
+                        .setInitialOptions(new Options())
646
+                        .createStackController());
649 647
         Options optionsToMerge = new Options();
650 648
         Component component = mock(Component.class);
651 649
         uut.mergeChildOptions(optionsToMerge, component);
652
-        verify(stackLayout[0], times(1)).mergeChildOptions(optionsToMerge, component);
650
+        verify(uut, times(1)).mergeChildOptions(optionsToMerge, component);
653 651
     }
654 652
 
655 653
     @Test
656 654
     public void mergeChildOptions_updatesParentControllerWithNewOptions() {
657
-        final StackLayout[] stackLayout = new StackLayout[1];
658
-        StackController uut =
659
-                new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "stack", new Options()) {
660
-                    @NonNull
661
-                    @Override
662
-                    protected StackLayout createView() {
663
-                        stackLayout[0] = spy(super.createView());
664
-                        return stackLayout[0];
665
-                    }
666
-                };
655
+        StackController uut = new StackControllerBuilder(activity)
656
+                        .setTopBarButtonCreator(new TopBarButtonCreatorMock())
657
+                        .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
658
+                        .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
659
+                        .setTopBarController(new TopBarController())
660
+                        .setId("stack")
661
+                        .setInitialOptions(new Options())
662
+                        .createStackController();
667 663
         ParentController parentController = Mockito.mock(ParentController.class);
668 664
         uut.setParentController(parentController);
669 665
         Options optionsToMerge = new Options();
@@ -731,18 +727,14 @@ public class StackControllerTest extends BaseTest {
731 727
                 return topBar;
732 728
             }
733 729
         });
734
-        return new StackController(activity,
735
-                new TopBarButtonCreatorMock(),
736
-                new TitleBarReactViewCreatorMock(),
737
-                new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()),
738
-                topBarController,
739
-                id,
740
-                new Options()) {
741
-            @Override
742
-            NavigationAnimator createAnimator() {
743
-                animator = Mockito.mock(NavigationAnimator.class);
744
-                return animator;
745
-            }
746
-        };
730
+        return new StackControllerBuilder(activity)
731
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
732
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
733
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
734
+                .setTopBarController(topBarController)
735
+                .setAnimator(animator)
736
+                .setId(id)
737
+                .setInitialOptions(new Options())
738
+                .createStackController();
747 739
     }
748 740
 }

+ 8
- 7
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarButtonControllerTest.java View File

@@ -44,13 +44,14 @@ public class TopBarButtonControllerTest extends BaseTest {
44 44
         final Activity activity = newActivity();
45 45
 
46 46
         TopBarButtonCreatorMock buttonCreatorMock = new TopBarButtonCreatorMock();
47
-        stackController = spy(new StackController(activity,
48
-                buttonCreatorMock,
49
-                new TitleBarReactViewCreatorMock(),
50
-                new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()),
51
-                new TopBarController(),
52
-                "stack",
53
-                new Options())
47
+        stackController = spy(new StackControllerBuilder(activity)
48
+                .setTopBarButtonCreator(buttonCreatorMock)
49
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
50
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
51
+                .setTopBarController(new TopBarController())
52
+                .setId("stack")
53
+                .setInitialOptions(new Options())
54
+                .createStackController()
54 55
         );
55 56
         stackController.getView().layout(0, 0, 1080, 1920);
56 57
         stackController.getTopBar().layout(0, 0, 1080, 200);

+ 8
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java View File

@@ -70,7 +70,14 @@ public class TopTabsViewControllerTest extends BaseTest {
70 70
 
71 71
     @NonNull
72 72
     private StackController createStackController(String id) {
73
-        return new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), id, new Options());
73
+        return new StackControllerBuilder(activity)
74
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
75
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
76
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
77
+                .setTopBarController(new TopBarController())
78
+                .setId(id)
79
+                .setInitialOptions(new Options())
80
+                .createStackController();
74 81
     }
75 82
 
76 83
     @NonNull

+ 9
- 2
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ViewControllerTest.java View File

@@ -70,14 +70,21 @@ public class ViewControllerTest extends BaseTest {
70 70
     @Test
71 71
     public void holdsAReferenceToStackControllerOrNull() {
72 72
         assertThat(uut.getParentController()).isNull();
73
-        StackController nav = new StackController(activity, new TopBarButtonCreatorMock(), new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()), new TopBarController(), "stack", new Options());
73
+        StackController nav = new StackControllerBuilder(activity)
74
+                .setTopBarButtonCreator(new TopBarButtonCreatorMock())
75
+                .setTitleBarReactViewCreator(new TitleBarReactViewCreatorMock())
76
+                .setTopBarBackgroundViewController(new TopBarBackgroundViewController(activity, new TopBarBackgroundViewCreatorMock()))
77
+                .setTopBarController(new TopBarController())
78
+                .setId("stack")
79
+                .setInitialOptions(new Options())
80
+                .createStackController();
74 81
         nav.push(uut, new CommandListenerAdapter());
75 82
         assertThat(uut.getParentController()).isEqualTo(nav);
76 83
     }
77 84
 
78 85
     @Test
79 86
     public void handleBackDefaultFalse() {
80
-        assertThat(uut.handleBack()).isFalse();
87
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
81 88
     }
82 89
 
83 90
     @Test

+ 37
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalAnimatorMock.java View File

@@ -0,0 +1,37 @@
1
+package com.reactnativenavigation.viewcontrollers.modal;
2
+
3
+import android.animation.AnimatorListenerAdapter;
4
+import android.content.Context;
5
+import android.view.View;
6
+
7
+import com.reactnativenavigation.anim.ModalAnimator2;
8
+import com.reactnativenavigation.parse.AnimationOptions;
9
+
10
+public class ModalAnimatorMock extends ModalAnimator2 {
11
+
12
+    ModalAnimatorMock(Context context) {
13
+        super(context);
14
+    }
15
+
16
+    @Override
17
+    public void show(View view, AnimationOptions animation, AnimatorListenerAdapter listener) {
18
+        try {
19
+            listener.onAnimationStart(null);
20
+            Thread.sleep(10);
21
+            listener.onAnimationEnd(null);
22
+        } catch (InterruptedException e) {
23
+            throw new RuntimeException(e);
24
+        }
25
+    }
26
+
27
+    @Override
28
+    public void dismiss(View view, AnimatorListenerAdapter listener) {
29
+        try {
30
+            listener.onAnimationStart(null);
31
+            Thread.sleep(10);
32
+            listener.onAnimationEnd(null);
33
+        } catch (InterruptedException e) {
34
+            throw new RuntimeException(e);
35
+        }
36
+    }
37
+}

+ 142
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java View File

@@ -0,0 +1,142 @@
1
+package com.reactnativenavigation.viewcontrollers.modal;
2
+
3
+import android.app.Activity;
4
+import android.widget.FrameLayout;
5
+
6
+import com.reactnativenavigation.BaseTest;
7
+import com.reactnativenavigation.anim.ModalAnimator2;
8
+import com.reactnativenavigation.mocks.SimpleViewController;
9
+import com.reactnativenavigation.parse.Options;
10
+import com.reactnativenavigation.utils.CommandListenerAdapter;
11
+import com.reactnativenavigation.viewcontrollers.Navigator.CommandListener;
12
+import com.reactnativenavigation.viewcontrollers.ViewController;
13
+
14
+import org.junit.Test;
15
+
16
+import static org.assertj.core.api.Java6Assertions.assertThat;
17
+import static org.mockito.ArgumentMatchers.any;
18
+import static org.mockito.ArgumentMatchers.eq;
19
+import static org.mockito.Mockito.spy;
20
+import static org.mockito.Mockito.times;
21
+import static org.mockito.Mockito.verify;
22
+
23
+public class ModalPresenterTest extends BaseTest {
24
+    private static final String MODAL_ID_1 = "modalId1";
25
+    private static final String MODAL_ID_2 = "modalId2";
26
+
27
+    private ViewController modal1;
28
+    private ViewController modal2;
29
+    private ModalPresenter uut;
30
+    private FrameLayout contentLayout;
31
+    private ModalAnimator2 animator;
32
+
33
+    @Override
34
+    public void beforeEach() {
35
+        Activity activity = newActivity();
36
+        animator = spy(new ModalAnimator2(activity));
37
+        uut = new ModalPresenter(animator);
38
+        contentLayout = new FrameLayout(activity);
39
+        activity.setContentView(contentLayout);
40
+        uut.setContentLayout(contentLayout);
41
+        modal1 = spy(new SimpleViewController(activity, MODAL_ID_1, new Options()));
42
+        modal2 = spy(new SimpleViewController(activity, MODAL_ID_2, new Options()));
43
+    }
44
+
45
+    @Test
46
+    public void showModal_noAnimation() {
47
+        disableShowModalAnimation(modal1);
48
+        CommandListener listener = spy(new CommandListenerAdapter() {
49
+            @Override
50
+            public void onSuccess(String childId) {
51
+                assertThat(modal1.getView().getParent()).isEqualTo(contentLayout);
52
+                verify(modal1, times(1)).onViewAppeared();
53
+            }
54
+        });
55
+        uut.showModal(modal1, null, listener);
56
+        verify(animator, times(0)).show(
57
+                eq(modal1.getView()),
58
+                eq(modal1.options.animations.showModal),
59
+                any()
60
+        );
61
+        verify(listener, times(1)).onSuccess(MODAL_ID_1);
62
+    }
63
+
64
+    @Test
65
+    public void showModal_previousModalIsRemovedFromHierarchy() {
66
+        uut.showModal(modal1, null, new CommandListenerAdapter() {
67
+            @Override
68
+            public void onSuccess(String childId) {
69
+                uut.showModal(modal2, modal1, new CommandListenerAdapter() {
70
+                    @Override
71
+                    public void onSuccess(String childId) {
72
+                        assertThat(modal1.getView().getParent()).isNull();
73
+                        verify(modal1, times(1)).onViewDisappear();
74
+                    }
75
+                });
76
+                assertThat(modal1.getView().getParent()).isEqualTo(modal2.getView().getParent());
77
+            }
78
+        });
79
+    }
80
+
81
+    @Test
82
+    public void showModal_animatesByDefault() {
83
+        uut.showModal(modal1, null, new CommandListenerAdapter() {
84
+            @Override
85
+            public void onSuccess(String childId) {
86
+                verify(animator, times(1)).show(
87
+                        eq(modal1.getView()),
88
+                        eq(modal1.options.animations.showModal),
89
+                        any()
90
+                );
91
+                assertThat(animator.isRunning()).isFalse();
92
+            }
93
+        });
94
+    }
95
+
96
+    @Test
97
+    public void dismissModal_animatesByDefault() {
98
+        disableShowModalAnimation(modal1);
99
+
100
+        uut.showModal(modal1, null, new CommandListenerAdapter());
101
+        uut.dismissModal(modal1, null, new CommandListenerAdapter() {
102
+            @Override
103
+            public void onSuccess(String childId) {
104
+                verify(modal1, times(1)).onViewDisappear();
105
+                verify(modal1, times(1)).destroy();
106
+            }
107
+        });
108
+
109
+        verify(animator, times(1)).dismiss(eq(modal1.getView()), any());
110
+    }
111
+
112
+    @Test
113
+    public void dismissModal_previousModalIsAddedAtIndex0() {
114
+        FrameLayout spy = spy(new FrameLayout(newActivity()));
115
+        uut.setContentLayout(spy);
116
+        uut.dismissModal(modal1, modal2, new CommandListenerAdapter());
117
+        verify(spy, times(1)).addView(modal2.getView(), 0);
118
+    }
119
+
120
+    @Test
121
+    public void dismissModal_noAnimation() {
122
+        disableShowModalAnimation(modal1);
123
+        disableDismissModalAnimation(modal1);
124
+
125
+        uut.showModal(modal1, null, new CommandListenerAdapter());
126
+        uut.dismissModal(modal1, null, new CommandListenerAdapter());
127
+        verify(modal1, times(1)).onViewDisappear();
128
+        verify(modal1, times(1)).destroy();
129
+        verify(animator, times(0)).dismiss(any(), any());
130
+    }
131
+
132
+    @Test
133
+    public void dismissModal_previousModalIsAddedBackToHierarchy() {
134
+        disableShowModalAnimation(modal1, modal2);
135
+
136
+        uut.showModal(modal1, null, new CommandListenerAdapter());
137
+        uut.showModal(modal2, modal1, new CommandListenerAdapter());
138
+        assertThat(modal1.getView().getParent()).isNull();
139
+        uut.dismissModal(modal2, modal1, new CommandListenerAdapter());
140
+        verify(modal1, times(2)).onViewAppeared();
141
+    }
142
+}

+ 223
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest2.java View File

@@ -0,0 +1,223 @@
1
+package com.reactnativenavigation.viewcontrollers.modal;
2
+
3
+import android.app.Activity;
4
+import android.view.ViewGroup;
5
+import android.widget.FrameLayout;
6
+
7
+import com.reactnativenavigation.BaseTest;
8
+import com.reactnativenavigation.anim.ModalAnimator2;
9
+import com.reactnativenavigation.mocks.SimpleViewController;
10
+import com.reactnativenavigation.parse.Options;
11
+import com.reactnativenavigation.utils.CommandListenerAdapter;
12
+import com.reactnativenavigation.viewcontrollers.Navigator.CommandListener;
13
+import com.reactnativenavigation.viewcontrollers.ViewController;
14
+
15
+import org.junit.Test;
16
+
17
+import java.util.EmptyStackException;
18
+
19
+import static org.assertj.core.api.Java6Assertions.assertThat;
20
+import static org.assertj.core.api.Java6Assertions.assertThatThrownBy;
21
+import static org.mockito.ArgumentMatchers.any;
22
+import static org.mockito.ArgumentMatchers.anyString;
23
+import static org.mockito.ArgumentMatchers.eq;
24
+import static org.mockito.Mockito.spy;
25
+import static org.mockito.Mockito.times;
26
+import static org.mockito.Mockito.verify;
27
+import static org.mockito.Mockito.verifyZeroInteractions;
28
+
29
+public class ModalStackTest2 extends BaseTest {
30
+    private static final String MODAL_ID_1 = "modalId1";
31
+    private static final String MODAL_ID_2 = "modalId2";
32
+    private static final String MODAL_ID_3 = "modalId3";
33
+
34
+    private ModalStack2 uut;
35
+    private ViewController modal1;
36
+    private ViewController modal2;
37
+    private ViewController modal3;
38
+    private Activity activity;
39
+    private ModalPresenter presenter;
40
+    private ModalAnimator2 animator;
41
+
42
+    @Override
43
+    public void beforeEach() {
44
+        activity = newActivity();
45
+        ViewGroup root = new FrameLayout(activity);
46
+        activity.setContentView(root);
47
+        animator = spy(new ModalAnimatorMock(activity));
48
+        presenter = spy(new ModalPresenter(animator));
49
+        uut = new ModalStack2(presenter);
50
+        uut.setContentLayout(root);
51
+        modal1 = spy(new SimpleViewController(activity, MODAL_ID_1, new Options()));
52
+        modal2 = spy(new SimpleViewController(activity, MODAL_ID_2, new Options()));
53
+        modal3 = spy(new SimpleViewController(activity, MODAL_ID_3, new Options()));
54
+    }
55
+
56
+    @Test
57
+    public void modalRefIsSaved() {
58
+        disableShowModalAnimation(modal1);
59
+        CommandListener listener = spy(new CommandListenerAdapter());
60
+        uut.showModal(modal1, listener);
61
+        verify(listener, times(1)).onSuccess(modal1.getId());
62
+        assertThat(findModal(MODAL_ID_1)).isNotNull();
63
+    }
64
+
65
+    @Test
66
+    public void showModal() {
67
+        CommandListener listener = spy(new CommandListenerAdapter());
68
+        uut.showModal(modal1, listener);
69
+        verify(listener, times(1)).onSuccess(modal1.getId());
70
+        assertThat(uut.size()).isOne();
71
+        verify(presenter, times(1)).showModal(modal1, null, listener);
72
+        assertThat(findModal(MODAL_ID_1)).isNotNull();
73
+    }
74
+
75
+    @Test
76
+    public void dismissModal() {
77
+        uut.showModal(modal1, new CommandListenerAdapter());
78
+        CommandListener listener = new CommandListenerAdapter();
79
+        uut.dismissModal(modal1.getId(), listener);
80
+        assertThat(findModal(modal1.getId())).isNull();
81
+        verify(presenter, times(1)).dismissModal(modal1, null, listener);
82
+    }
83
+
84
+    @Test
85
+    public void dismissModal_rejectIfModalNotFound() {
86
+        CommandListener listener = spy(new CommandListenerAdapter());
87
+        uut.dismissModal(MODAL_ID_1, listener);
88
+        verify(listener, times(1)).onError(anyString());
89
+        verifyZeroInteractions(listener);
90
+    }
91
+
92
+    @Test
93
+    public void dismissAllModals() {
94
+        uut.showModal(modal1, new CommandListenerAdapter());
95
+        uut.showModal(modal2, new CommandListenerAdapter());
96
+        CommandListener listener = spy(new CommandListenerAdapter() {
97
+            @Override
98
+            public void onSuccess(String childId) {
99
+                assertThat(findModal(modal1.getId())).isNull();
100
+                assertThat(findModal(modal2.getId())).isNull();
101
+                assertThat(uut.isEmpty()).isTrue();
102
+            }
103
+        });
104
+        uut.dismissAllModals(listener);
105
+        verify(listener, times(1)).onSuccess(anyString());
106
+        verifyZeroInteractions(listener);
107
+    }
108
+
109
+    @Test
110
+    public void dismissAllModals_rejectIfEmpty() {
111
+        CommandListener spy = spy(new CommandListenerAdapter());
112
+        uut.dismissAllModals(spy);
113
+        verify(spy, times(1)).onError(any());
114
+    }
115
+
116
+    @Test
117
+    public void dismissAllModals_onlyTopModalIsAnimated() {
118
+        uut.showModal(modal1, new CommandListenerAdapter());
119
+        uut.showModal(modal2, new CommandListenerAdapter());
120
+
121
+        ViewGroup view1 = modal1.getView();
122
+        ViewGroup view2 = modal2.getView();
123
+        CommandListener listener = spy(new CommandListenerAdapter());
124
+        uut.dismissAllModals(listener);
125
+
126
+        verify(presenter, times(1)).dismissModal(modal2, null, listener);
127
+        verify(animator, times(0)).dismiss(eq(view1), any());
128
+        verify(animator, times(1)).dismiss(eq(view2), any());
129
+        assertThat(uut.size()).isEqualTo(0);
130
+    }
131
+
132
+    @Test
133
+    public void dismissAllModals_bottomModalsAreDestroyed() {
134
+        uut.showModal(modal1, new CommandListenerAdapter());
135
+        uut.showModal(modal2, new CommandListenerAdapter());
136
+
137
+        uut.dismissAllModals(new CommandListenerAdapter());
138
+
139
+        verify(modal1, times(1)).destroy();
140
+        verify(modal1, times(1)).onViewDisappear();
141
+        assertThat(uut.size()).isEqualTo(0);
142
+    }
143
+
144
+    @Test
145
+    public void isEmpty() {
146
+        assertThat(uut.isEmpty()).isTrue();
147
+        uut.showModal(modal1, new CommandListenerAdapter());
148
+        assertThat(uut.isEmpty()).isFalse();
149
+        uut.dismissAllModals(new CommandListenerAdapter());
150
+        assertThat(uut.isEmpty()).isTrue();
151
+    }
152
+
153
+    @Test
154
+    public void peek() {
155
+        assertThat(uut.isEmpty()).isTrue();
156
+        assertThatThrownBy(() -> uut.peek()).isInstanceOf(EmptyStackException.class);
157
+        uut.showModal(modal1, new CommandListenerAdapter() {
158
+            @Override
159
+            public void onSuccess(String childId) {
160
+                assertThat(uut.peek()).isEqualTo(modal1);
161
+            }
162
+        });
163
+    }
164
+
165
+    @Test
166
+    public void onDismiss_onViewAppearedInvokedOnPreviousModal() {
167
+        disableShowModalAnimation(modal1, modal2);
168
+
169
+        uut.showModal(modal1, new CommandListenerAdapter());
170
+        uut.showModal(modal2, new CommandListenerAdapter());
171
+        uut.dismissModal(modal2.getId(), new CommandListenerAdapter());
172
+        verify(modal1, times(2)).onViewAppeared();
173
+    }
174
+
175
+    @Test
176
+    public void onDismiss_dismissModalInTheMiddleOfStack() {
177
+        disableShowModalAnimation(modal1, modal2, modal3);
178
+        disableDismissModalAnimation(modal1, modal2, modal3);
179
+
180
+        uut.showModal(modal1, new CommandListenerAdapter());
181
+        uut.showModal(modal2, new CommandListenerAdapter());
182
+        uut.showModal(modal3, new CommandListenerAdapter());
183
+
184
+        uut.dismissModal(modal2.getId(), new CommandListenerAdapter());
185
+        assertThat(uut.size()).isEqualTo(2);
186
+        verify(modal2, times(1)).onViewDisappear();
187
+        verify(modal2, times(1)).destroy();
188
+        assertThat(modal1.getView().getParent()).isNull();
189
+    }
190
+
191
+    @Test
192
+    public void handleBack_doesNothingIfModalStackIsEmpty() {
193
+        assertThat(uut.isEmpty()).isTrue();
194
+        assertThat(uut.handleBack(new CommandListenerAdapter(), () -> {})).isFalse();
195
+    }
196
+
197
+    @Test
198
+    public void handleBack_dismissModal() {
199
+        disableDismissModalAnimation(modal1);
200
+        uut.showModal(modal1, new CommandListenerAdapter());
201
+        assertThat(uut.handleBack(new CommandListenerAdapter(), () -> {})).isTrue();
202
+        verify(modal1, times(1)).onViewDisappear();
203
+
204
+    }
205
+
206
+    @Test
207
+    public void handleBack_ViewControllerTakesPrecedenceOverModal() {
208
+        ViewController backHandlingModal = spy(new SimpleViewController(activity, "stack", new Options()){
209
+            @Override
210
+            public boolean handleBack(CommandListener listener) {
211
+                return true;
212
+            }
213
+        });
214
+        uut.showModal(backHandlingModal, new CommandListenerAdapter());
215
+        assertThat(uut.handleBack(new CommandListenerAdapter(), any())).isTrue();
216
+        verify(backHandlingModal, times(1)).handleBack(any());
217
+        verify(backHandlingModal, times(0)).onViewDisappear();
218
+    }
219
+
220
+    private ViewController findModal(String id) {
221
+        return uut.findControllerById(id);
222
+    }
223
+}