diff --git a/e2e/BottomTabs.test.js b/e2e/BottomTabs.test.js index e9bc7412c90..6135759801f 100644 --- a/e2e/BottomTabs.test.js +++ b/e2e/BottomTabs.test.js @@ -1,5 +1,6 @@ import Utils from './Utils'; import TestIDs from '../playground/src/testIDs'; +import Android from './AndroidUtils'; const { elementByLabel, elementById } = Utils; @@ -106,4 +107,35 @@ describe('BottomTabs', () => { await elementById(TestIDs.POP_BTN).tap(); await expect(elementById(TestIDs.BOTTOM_TABS)).toBeVisible(); }); + + it('invoke bottomTabPressed event', async () => { + await elementById(TestIDs.THIRD_TAB_BAR_BTN).tap(); + await expect(elementByLabel('BottomTabPressed')).toBeVisible(); + await elementByLabel('OK').tap(); + await expect(elementByLabel('First Tab')).toBeVisible(); + }); + + it.e2e(':android: hardware back tab selection history', async () => { + await elementById(TestIDs.SECOND_TAB_BAR_BTN).tap(); + await elementById(TestIDs.FIRST_TAB_BAR_BUTTON).tap(); + await elementById(TestIDs.SECOND_TAB_BAR_BTN).tap(); + await elementById(TestIDs.SECOND_TAB_BAR_BTN).tap(); + await elementById(TestIDs.FIRST_TAB_BAR_BUTTON).tap(); + + Android.pressBack(); + await expect(elementByLabel('Second Tab')).toBeVisible(); + + Android.pressBack(); + await expect(elementByLabel('First Tab')).toBeVisible(); + + Android.pressBack(); + await expect(elementByLabel('Second Tab')).toBeVisible(); + + Android.pressBack(); + await expect(elementByLabel('First Tab')).toBeVisible(); + + Android.pressBack(); + await expect(elementByLabel('First Tab')).toBeNotVisible(); + await expect(elementByLabel('Second Tab')).toBeNotVisible(); + }); }); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/options/HardwareBackButtonOptions.kt b/lib/android/app/src/main/java/com/reactnativenavigation/options/HardwareBackButtonOptions.kt index 4aeee3225d8..1ece4e921e7 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/options/HardwareBackButtonOptions.kt +++ b/lib/android/app/src/main/java/com/reactnativenavigation/options/HardwareBackButtonOptions.kt @@ -6,10 +6,40 @@ import com.reactnativenavigation.options.parsers.BoolParser import org.json.JSONObject +sealed class HwBackBottomTabsBehaviour { + object Undefined : HwBackBottomTabsBehaviour() { + override fun hasValue(): Boolean = false + } + + object Exit : HwBackBottomTabsBehaviour() + object PrevSelection : HwBackBottomTabsBehaviour() + object JumpToFirst : HwBackBottomTabsBehaviour() + + open fun hasValue(): Boolean = true + + companion object { + private const val BEHAVIOUR_EXIT = "exit" + private const val BEHAVIOUR_PREV = "previous" + private const val BEHAVIOUR_FIRST = "first" + fun fromString(behaviour: String?): HwBackBottomTabsBehaviour { + return when (behaviour) { + BEHAVIOUR_PREV -> PrevSelection + BEHAVIOUR_FIRST -> JumpToFirst + BEHAVIOUR_EXIT -> Exit + else -> Undefined + } + } + } +} + open class HardwareBackButtonOptions(json: JSONObject? = null) { - @JvmField var dismissModalOnPress: Bool = NullBool() - @JvmField var popStackOnPress: Bool = NullBool() + @JvmField + var dismissModalOnPress: Bool = NullBool() + + @JvmField + var popStackOnPress: Bool = NullBool() + var bottomTabOnPress: HwBackBottomTabsBehaviour = HwBackBottomTabsBehaviour.Undefined init { parse(json) @@ -18,16 +48,19 @@ open class HardwareBackButtonOptions(json: JSONObject? = null) { fun mergeWith(other: HardwareBackButtonOptions) { if (other.dismissModalOnPress.hasValue()) dismissModalOnPress = other.dismissModalOnPress if (other.popStackOnPress.hasValue()) popStackOnPress = other.popStackOnPress + if (other.bottomTabOnPress.hasValue()) bottomTabOnPress = other.bottomTabOnPress } fun mergeWithDefault(defaultOptions: HardwareBackButtonOptions) { if (!dismissModalOnPress.hasValue()) dismissModalOnPress = defaultOptions.dismissModalOnPress if (!popStackOnPress.hasValue()) popStackOnPress = defaultOptions.popStackOnPress + if (!bottomTabOnPress.hasValue()) bottomTabOnPress = defaultOptions.bottomTabOnPress } private fun parse(json: JSONObject?) { json ?: return dismissModalOnPress = BoolParser.parse(json, "dismissModalOnPress") popStackOnPress = BoolParser.parse(json, "popStackOnPress") + bottomTabOnPress = HwBackBottomTabsBehaviour.fromString(json.optString("bottomTabsOnPress")) } } \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java index 9e38012f9ed..3683e51077c 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java @@ -13,6 +13,7 @@ import com.aurelhubert.ahbottomnavigation.AHBottomNavigation; import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem; import com.reactnativenavigation.options.BottomTabOptions; +import com.reactnativenavigation.options.HwBackBottomTabsBehaviour; import com.reactnativenavigation.options.Options; import com.reactnativenavigation.react.CommandListener; import com.reactnativenavigation.react.CommandListenerAdapter; @@ -29,6 +30,8 @@ import com.reactnativenavigation.views.bottomtabs.BottomTabsLayout; import java.util.Collection; +import java.util.Deque; +import java.util.LinkedList; import java.util.List; import static com.reactnativenavigation.utils.CollectionUtils.forEach; @@ -39,6 +42,7 @@ public class BottomTabsController extends ParentController imp private BottomTabsContainer bottomTabsContainer; private BottomTabs bottomTabs; + private final Deque selectionStack; private final List> tabs; private final EventEmitter eventEmitter; private final ImageLoader imageLoader; @@ -66,6 +70,7 @@ public BottomTabsController(Activity activity, List> tabs, Chi this.presenter = bottomTabsPresenter; this.tabPresenter = bottomTabPresenter; forEach(tabs, tab -> tab.setParentController(this)); + selectionStack = new LinkedList<>(); } @Override @@ -156,7 +161,27 @@ public void mergeChildOptions(Options options, ViewController child) { @Override public boolean handleBack(CommandListener listener) { - return !tabs.isEmpty() && tabs.get(bottomTabs.getCurrentItem()).handleBack(listener); + final boolean childBack = !tabs.isEmpty() && tabs.get(bottomTabs.getCurrentItem()).handleBack(listener); + final Options options = resolveCurrentOptions(); + if (!childBack) { + if (options.hardwareBack.getBottomTabOnPress() instanceof HwBackBottomTabsBehaviour.PrevSelection) { + if (!selectionStack.isEmpty()) { + final int prevSelectedTabIndex = selectionStack.poll(); + selectTab(prevSelectedTabIndex, false); + return true; + } + } else if (options.hardwareBack.getBottomTabOnPress() instanceof HwBackBottomTabsBehaviour.JumpToFirst) { + if (getSelectedIndex() != 0) { + selectTab(0, false); + return true; + } else { + return false; + } + } else { + return false; + } + } + return childBack; } @Override @@ -203,7 +228,7 @@ private List createTabs() { }); } - int getSelectedIndex() { + public int getSelectedIndex() { return bottomTabs.getCurrentItem(); } @@ -239,6 +264,12 @@ public void destroy() { @Override public void selectTab(final int newIndex) { + final boolean enableSelectionHistory = resolveCurrentOptions().hardwareBack.getBottomTabOnPress() instanceof HwBackBottomTabsBehaviour.PrevSelection; + selectTab(newIndex, enableSelectionHistory); + } + + private void selectTab(int newIndex, boolean enableSelectionHistory) { + saveTabSelection(newIndex, enableSelectionHistory); tabsAttacher.onTabSelected(tabs.get(newIndex)); getCurrentView().setVisibility(View.INVISIBLE); bottomTabs.setCurrentItem(newIndex, false); @@ -246,6 +277,15 @@ public void selectTab(final int newIndex) { getCurrentChild().onViewDidAppear(); } + private void saveTabSelection(int newIndex, boolean enableSelectionHistory) { + if (enableSelectionHistory) { + if (selectionStack.isEmpty() + || selectionStack.peek() != newIndex + || bottomTabs.getCurrentItem() != newIndex) + selectionStack.offerFirst(bottomTabs.getCurrentItem()); + } + } + @NonNull private ViewGroup getCurrentView() { return tabs.get(bottomTabs.getCurrentItem()).getView(); diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java deleted file mode 100644 index bb1ef6cbe57..00000000000 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java +++ /dev/null @@ -1,511 +0,0 @@ -package com.reactnativenavigation.viewcontrollers.bottomtabs; - -import android.app.Activity; -import android.graphics.Color; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup.MarginLayoutParams; - -import com.aurelhubert.ahbottomnavigation.AHBottomNavigation; -import com.reactnativenavigation.BaseTest; -import com.reactnativenavigation.TestUtils; -import com.reactnativenavigation.mocks.ImageLoaderMock; -import com.reactnativenavigation.mocks.SimpleViewController; -import com.reactnativenavigation.mocks.TypefaceLoaderMock; -import com.reactnativenavigation.options.BottomTabsOptions; -import com.reactnativenavigation.options.Options; -import com.reactnativenavigation.options.params.Bool; -import com.reactnativenavigation.options.params.Colour; -import com.reactnativenavigation.options.params.NullText; -import com.reactnativenavigation.options.params.Number; -import com.reactnativenavigation.options.params.ThemeColour; -import com.reactnativenavigation.options.params.Text; -import com.reactnativenavigation.react.CommandListenerAdapter; -import com.reactnativenavigation.react.events.EventEmitter; -import com.reactnativenavigation.utils.ImageLoader; -import com.reactnativenavigation.utils.OptionHelper; -import com.reactnativenavigation.utils.StatusBarUtils; -import com.reactnativenavigation.viewcontrollers.bottomtabs.attacher.BottomTabsAttacher; -import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry; -import com.reactnativenavigation.viewcontrollers.fakes.FakeParentController; -import com.reactnativenavigation.viewcontrollers.parent.ParentController; -import com.reactnativenavigation.viewcontrollers.stack.StackController; -import com.reactnativenavigation.viewcontrollers.viewcontroller.Presenter; -import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController; -import com.reactnativenavigation.views.bottomtabs.BottomTabs; -import com.reactnativenavigation.views.bottomtabs.BottomTabsContainer; -import com.reactnativenavigation.views.bottomtabs.BottomTabsLayout; - -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.coordinatorlayout.widget.CoordinatorLayout; - - -import static com.reactnativenavigation.TestUtils.hideBackButton; -import static com.reactnativenavigation.utils.ObjectUtils.perform; -import static org.assertj.core.api.Java6Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class BottomTabsControllerTest extends BaseTest { - - private Activity activity; - private BottomTabs bottomTabs; - private BottomTabsContainer bottomTabsContainer; - private BottomTabsController uut; - private final Options initialOptions = new Options(); - private ViewController child1; - private ViewController child2; - private ViewController child3; - private ViewController stackChild; - private StackController child4; - private ViewController child5; - private final Options tabOptions = OptionHelper.createBottomTabOptions(); - private final ImageLoader imageLoaderMock = ImageLoaderMock.mock(); - private EventEmitter eventEmitter; - private ChildControllersRegistry childRegistry; - private List> tabs; - private BottomTabsPresenter presenter; - private BottomTabPresenter bottomTabPresenter; - private BottomTabsAttacher tabsAttacher; - - @Override - public void beforeEach() { - activity = newActivity(); - childRegistry = new ChildControllersRegistry(); - eventEmitter = Mockito.mock(EventEmitter.class); - prepareViewsForTests(); - StatusBarUtils.saveStatusBarHeight(63); - } - - @Test - public void createView_checkProperStructure() { - idleMainLooper(); - assertThat(uut.getView()).isInstanceOf(CoordinatorLayout.class); - assertThat(uut.getView().getChildAt(uut.getView().getChildCount() - 1)).isInstanceOf(BottomTabsContainer.class); - assertThat(((CoordinatorLayout.LayoutParams) uut.getBottomTabsContainer().getLayoutParams()).gravity).isEqualTo(Gravity.BOTTOM); - } - - @Test - public void createView_tabsWithoutIconsAreAccepted() { - tabOptions.bottomTabOptions.icon = new NullText(); - prepareViewsForTests(); - assertThat(uut.getBottomTabs().getItemsCount()).isEqualTo(tabs.size()); - } - - @Test - public void createView_showTitlesWhenAllTabsDontHaveIcons() { - tabOptions.bottomTabOptions.icon = new NullText(); - assertThat(tabOptions.bottomTabsOptions.titleDisplayMode.hasValue()).isFalse(); - prepareViewsForTests(); - presenter.applyOptions(Options.EMPTY); - assertThat(bottomTabsContainer.getBottomTabs().getTitleState()).isEqualTo(AHBottomNavigation.TitleState.ALWAYS_SHOW); - } - - @Test(expected = RuntimeException.class) - public void setTabs_ThrowWhenMoreThan5() { - tabs.add(new SimpleViewController(activity, childRegistry, "6", tabOptions)); - createBottomTabs(); - } - - @Test - public void parentControllerIsSet() { - uut = createBottomTabs(); - for (ViewController tab : tabs) { - assertThat(tab.getParentController()).isEqualTo(uut); - } - } - - @Test - public void setTabs_allChildViewsAreAttachedToHierarchy() { - uut.onViewWillAppear(); - assertThat(uut.getView().getChildCount()).isEqualTo(6); - for (ViewController child : uut.getChildControllers()) { - assertThat(child.getView().getParent()).isNotNull(); - } - } - - @Test - public void setTabs_firstChildIsVisibleOtherAreGone() { - uut.onViewWillAppear(); - for (int i = 0; i < uut.getChildControllers().size(); i++) { - assertThat(uut.getView().getChildAt(i)).isEqualTo(tabs.get(i).getView()); - assertThat(uut.getView().getChildAt(i).getVisibility()).isEqualTo(i == 0 ? View.VISIBLE : View.INVISIBLE); - } - } - - @Test - public void onTabSelected() { - uut.ensureViewIsCreated(); - assertThat(uut.getSelectedIndex()).isZero(); - assertThat(((ViewController) ((List) uut.getChildControllers()).get(0)).getView().getVisibility()).isEqualTo(View.VISIBLE); - - uut.onTabSelected(3, false); - - assertThat(uut.getSelectedIndex()).isEqualTo(3); - assertThat(((ViewController) ((List) uut.getChildControllers()).get(0)).getView().getVisibility()).isEqualTo(View.INVISIBLE); - assertThat(((ViewController) ((List) uut.getChildControllers()).get(3)).getView().getVisibility()).isEqualTo(View.VISIBLE); - verify(eventEmitter).emitBottomTabSelected(0, 3); - } - - @Test - public void onTabReSelected() { - uut.ensureViewIsCreated(); - assertThat(uut.getSelectedIndex()).isZero(); - - uut.onTabSelected(0, true); - - assertThat(uut.getSelectedIndex()).isEqualTo(0); - assertThat(((ViewController) ((List) uut.getChildControllers()).get(0)).getView().getParent()).isNotNull(); - verify(eventEmitter).emitBottomTabSelected(0, 0); - } - - @Test - public void handleBack_DelegatesToSelectedChild() { - uut.ensureViewIsCreated(); - assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse(); - uut.selectTab(4); - assertThat(uut.handleBack(new CommandListenerAdapter())).isTrue(); - verify(child5).handleBack(any()); - } - - @Test - public void applyChildOptions_bottomTabsOptionsAreClearedAfterApply() { - ParentController parent = Mockito.mock(ParentController.class); - uut.setParentController(parent); - - child1.options.bottomTabsOptions.backgroundColor = new ThemeColour(new Colour(Color.RED)); - child1.onViewWillAppear(); - - ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(Options.class); - verify(parent).applyChildOptions(optionsCaptor.capture(), any()); - assertThat(optionsCaptor.getValue().bottomTabsOptions.backgroundColor.hasValue()).isFalse(); - } - - @Test - public void applyOptions_bottomTabsCreateViewOnlyOnce() { - idleMainLooper(); - verify(presenter).applyOptions(any()); - verify(bottomTabsContainer.getBottomTabs(), times(2)).superCreateItems(); // first time when view is created, second time when options are applied - } - - @Test - public void onSizeChanged_recreateItemsIfSizeHasChanged() { - int numberOfPreviousInvocations = 1; - bottomTabs.onSizeChanged(0, 0, 0, 0); - verify(bottomTabs, times(numberOfPreviousInvocations)).superCreateItems(); - - bottomTabs.onSizeChanged(100, 0, 0, 0); - verify(bottomTabs, times(numberOfPreviousInvocations)).superCreateItems(); - - bottomTabs.onSizeChanged(1080, 147, 0, 0); - verify(bottomTabs, times(numberOfPreviousInvocations + 1)).superCreateItems(); - - bottomTabs.onSizeChanged(1920, 147, 0, 0); - verify(bottomTabs, times(numberOfPreviousInvocations + 2)).superCreateItems(); - - when(bottomTabs.getItemsCount()).thenReturn(0); - bottomTabs.onSizeChanged(1080, 147, 0, 0); - verify(bottomTabs, times(numberOfPreviousInvocations + 2)).superCreateItems(); - } - - @Test - public void mergeOptions_currentTabIndex() { - uut.ensureViewIsCreated(); - assertThat(uut.getSelectedIndex()).isZero(); - - Options options = new Options(); - options.bottomTabsOptions.currentTabIndex = new Number(1); - uut.mergeOptions(options); - assertThat(uut.getSelectedIndex()).isOne(); - verify(eventEmitter, times(0)).emitBottomTabSelected(any(Integer.class), any(Integer.class)); - } - - @Test - public void mergeOptions_drawBehind() { - assertThat(uut.getBottomInset(child1)).isEqualTo(uut.getBottomTabs().getHeight()); - - Options o1 = new Options(); - o1.bottomTabsOptions.drawBehind = new Bool(true); - child1.mergeOptions(o1); - assertThat(uut.getBottomInset(child1)).isEqualTo(0); - - Options o2 = new Options(); - o2.topBar.title.text = new Text("Some text"); - child1.mergeOptions(o1); - assertThat(uut.getBottomInset(child1)).isEqualTo(0); - } - - @Test - public void mergeOptions_drawBehind_stack() { - uut.ensureViewIsCreated(); - uut.selectTab(3); - - assertThat(((MarginLayoutParams) stackChild.getView().getLayoutParams()).bottomMargin).isEqualTo(bottomTabs.getHeight()); - - Options o1 = new Options(); - o1.bottomTabsOptions.drawBehind = new Bool(true); - stackChild.mergeOptions(o1); - - assertThat(((MarginLayoutParams) stackChild.getView().getLayoutParams()).bottomMargin).isEqualTo(0); - } - - @Test - public void mergeOptions_mergesBottomTabOptions() { - Options options = new Options(); - uut.mergeOptions(options); - verify(bottomTabPresenter).mergeOptions(options); - } - - @Test - public void applyChildOptions_resolvedOptionsAreUsed() { - Options childOptions = new Options(); - SimpleViewController pushedScreen = new SimpleViewController(activity, childRegistry, "child4.1", childOptions); - disablePushAnimation(pushedScreen); - child4 = spyOnStack(pushedScreen); - - tabs = new ArrayList<>(Collections.singletonList(child4)); - tabsAttacher = new BottomTabsAttacher(tabs, presenter, Options.EMPTY); - - initialOptions.bottomTabsOptions.currentTabIndex = new Number(0); - Options resolvedOptions = new Options(); - uut = new BottomTabsController(activity, - tabs, - childRegistry, - eventEmitter, - imageLoaderMock, - "uut", - initialOptions, - new Presenter(activity, new Options()), - tabsAttacher, - presenter, - new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new TypefaceLoaderMock(), new Options())) { - @Override - public Options resolveCurrentOptions() { - return resolvedOptions; - } - - @NonNull - @Override - protected BottomTabs createBottomTabs() { - return new BottomTabs(activity) { - @Override - protected void createItems() { - - } - }; - } - }; - - activity.setContentView(uut.getView()); - idleMainLooper(); - verify(presenter, times(2)).applyChildOptions(eq(resolvedOptions), any()); - } - - @Test - public void child_mergeOptions_currentTabIndex() { - uut.ensureViewIsCreated(); - - assertThat(uut.getSelectedIndex()).isZero(); - - Options options = new Options(); - options.bottomTabsOptions.currentTabIndex = new Number(1); - child1.mergeOptions(options); - - assertThat(uut.getSelectedIndex()).isOne(); - } - - @Test - public void resolveCurrentOptions_returnsFirstTabIfInvokedBeforeViewIsCreated() { - uut = createBottomTabs(); - assertThat(uut.getCurrentChild()).isEqualTo(tabs.get(0)); - } - - @Test - public void buttonPressInvokedOnCurrentTab() { - uut.ensureViewIsCreated(); - uut.selectTab(4); - - uut.sendOnNavigationButtonPressed("btn1"); - verify(child5, times(1)).sendOnNavigationButtonPressed("btn1"); - } - - @Test - public void push() { - uut.selectTab(3); - - SimpleViewController stackChild2 = new SimpleViewController(activity, childRegistry, "stackChild2", new Options()); - disablePushAnimation(stackChild2); - hideBackButton(stackChild2); - - assertThat(child4.size()).isEqualTo(1); - child4.push(stackChild2, new CommandListenerAdapter()); - assertThat(child4.size()).isEqualTo(2); - } - - @Test - public void oneTimeOptionsAreAppliedOnce() { - Options options = new Options(); - options.bottomTabsOptions.currentTabIndex = new Number(1); - - assertThat(uut.getSelectedIndex()).isZero(); - uut.mergeOptions(options); - assertThat(uut.getSelectedIndex()).isOne(); - assertThat(uut.options.bottomTabsOptions.currentTabIndex.hasValue()).isFalse(); - assertThat(uut.initialOptions.bottomTabsOptions.currentTabIndex.hasValue()).isFalse(); - } - - @Test - public void selectTab() { - uut.selectTab(1); - verify(tabsAttacher).onTabSelected(tabs.get(1)); - } - - @Test - public void selectTab_onViewDidAppearIsInvokedAfterSelection() { - uut.selectTab(1); - verify(child2).onViewDidAppear(); - } - - @Test - public void creatingTabs_onViewDidAppearInvokedAfterInitialTabIndexSet() { - Options options = Options.EMPTY.copy(); - options.bottomTabsOptions.currentTabIndex = new Number(1); - prepareViewsForTests(options.bottomTabsOptions); - idleMainLooper(); - verify(tabs.get(0), times(0)).onViewDidAppear(); - verify(tabs.get(1), times(1)).onViewDidAppear(); - verify(tabs.get(2), times(0)).onViewDidAppear(); - verify(tabs.get(3), times(0)).onViewDidAppear(); - verify(tabs.get(4), times(0)).onViewDidAppear(); - } - - @Test - public void getTopInset() { - assertThat(child1.getTopInset()).isEqualTo(getStatusBarHeight()); - assertThat(child2.getTopInset()).isEqualTo(getStatusBarHeight()); - - child1.options.statusBar.drawBehind = new Bool(true); - assertThat(child1.getTopInset()).isEqualTo(0); - assertThat(child2.getTopInset()).isEqualTo(getStatusBarHeight()); - - assertThat(stackChild.getTopInset()).isEqualTo(getStatusBarHeight() + child4.getTopBar().getHeight()); - } - - @Test - public void getBottomInset_defaultOptionsAreTakenIntoAccount() { - Options defaultOptions = new Options(); - defaultOptions.bottomTabsOptions.visible = new Bool(false); - - assertThat(uut.getBottomInset(child1)).isEqualTo(bottomTabs.getHeight()); - uut.setDefaultOptions(defaultOptions); - assertThat(uut.getBottomInset(child1)).isZero(); - } - - @Test - public void destroy() { - uut.destroy(); - verify(tabsAttacher).destroy(); - } - - private void prepareViewsForTests() { - prepareViewsForTests(initialOptions.bottomTabsOptions); - } - - private void prepareViewsForTests(BottomTabsOptions bottomTabsOptions) { - perform(uut, ViewController::destroy); - bottomTabs = spy(new BottomTabs(activity) { - @Override - public void superCreateItems() { - - } - }); - bottomTabsContainer = spy(new BottomTabsContainer(activity, bottomTabs)); - - createChildren(); - tabs = Arrays.asList(child1, child2, child3, child4, child5); - initialOptions.bottomTabsOptions = bottomTabsOptions; - presenter = spy(new BottomTabsPresenter(tabs, initialOptions, new BottomTabsAnimator())); - bottomTabPresenter = spy(new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new TypefaceLoaderMock(), initialOptions)); - tabsAttacher = spy(new BottomTabsAttacher(tabs, presenter, initialOptions)); - uut = createBottomTabs(); - activity.setContentView(new FakeParentController(activity, childRegistry, uut).getView()); - } - - private void createChildren() { - child1 = spy(new SimpleViewController(activity, childRegistry, "child1", tabOptions)); - child2 = spy(new SimpleViewController(activity, childRegistry, "child2", tabOptions)); - child3 = spy(new SimpleViewController(activity, childRegistry, "child3", tabOptions)); - stackChild = spy(new SimpleViewController(activity, childRegistry, "stackChild", tabOptions)); - child4 = spyOnStack(stackChild); - child5 = spy(new SimpleViewController(activity, childRegistry, "child5", tabOptions)); - when(child5.handleBack(any())).thenReturn(true); - } - - private StackController spyOnStack(ViewController initialChild) { - StackController build = TestUtils.newStackController(activity) - .setInitialOptions(tabOptions) - .build(); - StackController stack = spy(build); - disablePushAnimation(initialChild); - stack.ensureViewIsCreated(); - stack.push(initialChild, new CommandListenerAdapter()); - return stack; - } - - private BottomTabsController createBottomTabs() { - return new BottomTabsController(activity, - tabs, - childRegistry, - eventEmitter, - imageLoaderMock, - "uut", - initialOptions, - new Presenter(activity, initialOptions), - tabsAttacher, - presenter, - bottomTabPresenter) { - @Override - public void ensureViewIsCreated() { - super.ensureViewIsCreated(); - uut.getView().layout(0, 0, 1000, 1000); - } - - @NonNull - @Override - public BottomTabsLayout createView() { - BottomTabsLayout view = super.createView(); - bottomTabs.getLayoutParams().height = 100; - return view; - } - - @NonNull - @Override - protected BottomTabsContainer createBottomTabsContainer() { - return bottomTabsContainer; - } - - @NonNull - @Override - protected BottomTabs createBottomTabs() { - return bottomTabs; - } - }; - } - - private int getStatusBarHeight() { - return StatusBarUtils.getStatusBarHeight(activity); - } -} diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.kt b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.kt new file mode 100644 index 00000000000..2d14b4ccde9 --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.kt @@ -0,0 +1,572 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs + +import android.app.Activity +import android.graphics.Color +import android.view.Gravity +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.aurelhubert.ahbottomnavigation.AHBottomNavigation.TitleState +import com.reactnativenavigation.BaseTest +import com.reactnativenavigation.TestUtils +import com.reactnativenavigation.mocks.ImageLoaderMock.mock +import com.reactnativenavigation.mocks.SimpleViewController +import com.reactnativenavigation.mocks.TypefaceLoaderMock +import com.reactnativenavigation.options.BottomTabsOptions +import com.reactnativenavigation.options.HwBackBottomTabsBehaviour +import com.reactnativenavigation.options.Options +import com.reactnativenavigation.options.params.* +import com.reactnativenavigation.react.CommandListenerAdapter +import com.reactnativenavigation.react.events.EventEmitter +import com.reactnativenavigation.utils.OptionHelper +import com.reactnativenavigation.utils.SystemUiUtils.getStatusBarHeight +import com.reactnativenavigation.utils.SystemUiUtils.saveStatusBarHeight +import com.reactnativenavigation.viewcontrollers.bottomtabs.attacher.BottomTabsAttacher +import com.reactnativenavigation.viewcontrollers.child.ChildControllersRegistry +import com.reactnativenavigation.viewcontrollers.fakes.FakeParentController +import com.reactnativenavigation.viewcontrollers.parent.ParentController +import com.reactnativenavigation.viewcontrollers.stack.StackController +import com.reactnativenavigation.viewcontrollers.viewcontroller.Presenter +import com.reactnativenavigation.viewcontrollers.viewcontroller.ViewController +import com.reactnativenavigation.views.bottomtabs.BottomTabs +import com.reactnativenavigation.views.bottomtabs.BottomTabsContainer +import com.reactnativenavigation.views.bottomtabs.BottomTabsLayout +import org.assertj.core.api.Java6Assertions +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.times +import java.util.* + +class BottomTabsControllerTest : BaseTest() { + private lateinit var activity: Activity + private lateinit var bottomTabs: BottomTabs + private lateinit var bottomTabsContainer: BottomTabsContainer + private lateinit var uut: BottomTabsController + private val initialOptions = Options() + private lateinit var child1: ViewController<*> + private lateinit var child2: ViewController<*> + private lateinit var child3: ViewController<*> + private lateinit var stackChild: ViewController<*> + private lateinit var child4: StackController + private lateinit var child5: ViewController<*> + private val tabOptions = OptionHelper.createBottomTabOptions() + private val imageLoaderMock = mock() + private lateinit var eventEmitter: EventEmitter + private lateinit var childRegistry: ChildControllersRegistry + private lateinit var tabs: MutableList> + private lateinit var presenter: BottomTabsPresenter + private lateinit var bottomTabPresenter: BottomTabPresenter + private lateinit var tabsAttacher: BottomTabsAttacher + override fun beforeEach() { + super.beforeEach() + activity = newActivity() + childRegistry = ChildControllersRegistry() + eventEmitter = Mockito.mock(EventEmitter::class.java) + prepareViewsForTests() + saveStatusBarHeight(63) + } + + @Test + fun createView_checkProperStructure() { + idleMainLooper() + Java6Assertions.assertThat(uut.view).isInstanceOf(CoordinatorLayout::class.java) + Java6Assertions.assertThat(uut.view.getChildAt(uut.view.childCount - 1)).isInstanceOf( + BottomTabsContainer::class.java + ) + Java6Assertions.assertThat((uut.bottomTabsContainer.layoutParams as CoordinatorLayout.LayoutParams).gravity) + .isEqualTo(Gravity.BOTTOM) + } + + @Test + fun createView_tabsWithoutIconsAreAccepted() { + tabOptions.bottomTabOptions.icon = NullText() + prepareViewsForTests() + Java6Assertions.assertThat(uut.bottomTabs.itemsCount).isEqualTo(tabs.size) + } + + @Test + fun createView_showTitlesWhenAllTabsDontHaveIcons() { + tabOptions.bottomTabOptions.icon = NullText() + Java6Assertions.assertThat(tabOptions.bottomTabsOptions.titleDisplayMode.hasValue()).isFalse + prepareViewsForTests() + presenter.applyOptions(Options.EMPTY) + Java6Assertions.assertThat(bottomTabsContainer.bottomTabs.titleState).isEqualTo(TitleState.ALWAYS_SHOW) + } + + @Test(expected = RuntimeException::class) + fun setTabs_ThrowWhenMoreThan5() { + tabs.add(SimpleViewController(activity, childRegistry, "6", tabOptions)) + createBottomTabs() + idleMainLooper() + } + + @Test + fun parentControllerIsSet() { + uut = createBottomTabs() + for (tab in tabs) { + Java6Assertions.assertThat(tab.parentController).isEqualTo(uut) + } + } + + @Test + fun setTabs_allChildViewsAreAttachedToHierarchy() { + uut.onViewWillAppear() + Java6Assertions.assertThat(uut.view.childCount).isEqualTo(6) + for (child in uut.childControllers) { + Java6Assertions.assertThat(child.view.parent).isNotNull + } + } + + @Test + fun setTabs_firstChildIsVisibleOtherAreGone() { + uut.onViewWillAppear() + for (i in uut.childControllers.indices) { + Java6Assertions.assertThat(uut.view.getChildAt(i)).isEqualTo(tabs[i].view) + Java6Assertions.assertThat(uut.view.getChildAt(i).visibility) + .isEqualTo(if (i == 0) View.VISIBLE else View.INVISIBLE) + } + } + + @Test + fun onTabSelected() { + uut.ensureViewIsCreated() + Java6Assertions.assertThat(uut.selectedIndex).isZero + Java6Assertions.assertThat(((uut.childControllers as List<*>)[0] as ViewController<*>).view.visibility) + .isEqualTo( + View.VISIBLE + ) + uut.onTabSelected(3, false) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(3) + Java6Assertions.assertThat(((uut.childControllers as List<*>)[0] as ViewController<*>).view.visibility) + .isEqualTo( + View.INVISIBLE + ) + Java6Assertions.assertThat(((uut.childControllers as List<*>)[3] as ViewController<*>).view.visibility) + .isEqualTo( + View.VISIBLE + ) + Mockito.verify(eventEmitter).emitBottomTabSelected(0, 3) + } + + @Test + fun onTabReSelected() { + uut.ensureViewIsCreated() + Java6Assertions.assertThat(uut.selectedIndex).isZero + uut.onTabSelected(0, true) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(0) + Java6Assertions.assertThat(((uut.childControllers as List<*>)[0] as ViewController<*>).view.parent).isNotNull + Mockito.verify(eventEmitter).emitBottomTabSelected(0, 0) + } + + @Test + fun handleBack_DelegatesToSelectedChild() { + uut.ensureViewIsCreated() + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isFalse + uut.selectTab(4) + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isTrue + Mockito.verify(child5).handleBack(ArgumentMatchers.any()) + } + + @Test + fun `handleBack - PrevSelection - reselect tab selection history of navigation when root has bottom tabs`() { + val options = Options().apply { + hardwareBack.bottomTabOnPress = HwBackBottomTabsBehaviour.PrevSelection + } + prepareViewsForTests(options = options) + idleMainLooper() + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(0) + + uut.selectTab(1) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(1) + + uut.selectTab(3) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(3) + + uut.selectTab(2) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(2) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isTrue + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(3) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isTrue + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(1) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isTrue + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(0) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isFalse + } + + @Test + fun `handleBack - JumpToFirst - reselect first tab`() { + val options = Options().apply { + hardwareBack.bottomTabOnPress = HwBackBottomTabsBehaviour.JumpToFirst + } + prepareViewsForTests(options = options) + idleMainLooper() + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(0) + + uut.selectTab(1) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(1) + + uut.selectTab(3) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(3) + + uut.selectTab(2) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(2) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isTrue + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(0) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isFalse + } + + @Test + fun `handleBack - Default - should exit app with no reselection`() { + + prepareViewsForTests() + idleMainLooper() + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(0) + + uut.selectTab(1) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(1) + + uut.selectTab(3) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(3) + + uut.selectTab(2) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(2) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isFalse + } + + @Test + fun `handleBack - Exit - reselect first tab`() { + val options = Options().apply { + hardwareBack.bottomTabOnPress = HwBackBottomTabsBehaviour.Exit + } + prepareViewsForTests(options = options) + idleMainLooper() + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(0) + + uut.selectTab(1) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(1) + + uut.selectTab(3) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(3) + + uut.selectTab(2) + Java6Assertions.assertThat(uut.selectedIndex).isEqualTo(2) + + Java6Assertions.assertThat(uut.handleBack(CommandListenerAdapter())).isFalse + } + + @Test + fun applyChildOptions_bottomTabsOptionsAreClearedAfterApply() { + val parent = Mockito.mock(ParentController::class.java) + uut.parentController = parent + child1.options.bottomTabsOptions.backgroundColor = ThemeColour(Colour(Color.RED)) + child1.onViewWillAppear() + val optionsCaptor = ArgumentCaptor.forClass( + Options::class.java + ) + Mockito.verify(parent).applyChildOptions(optionsCaptor.capture(), ArgumentMatchers.any()) + Java6Assertions.assertThat(optionsCaptor.value.bottomTabsOptions.backgroundColor.hasValue()).isFalse + } + + @Test + fun applyOptions_bottomTabsCreateViewOnlyOnce() { + idleMainLooper() + Mockito.verify(presenter).applyOptions(any()) + Mockito.verify(bottomTabsContainer.bottomTabs, times(2)) + .superCreateItems() // first time when view is created, second time when options are applied + } + + @Test + fun onSizeChanged_recreateItemsIfSizeHasChanged() { + val numberOfPreviousInvocations = 1 + bottomTabs.onSizeChanged(0, 0, 0, 0) + Mockito.verify(bottomTabs, Mockito.times(numberOfPreviousInvocations)).superCreateItems() + bottomTabs.onSizeChanged(100, 0, 0, 0) + Mockito.verify(bottomTabs, Mockito.times(numberOfPreviousInvocations)).superCreateItems() + bottomTabs.onSizeChanged(1080, 147, 0, 0) + Mockito.verify(bottomTabs, Mockito.times(numberOfPreviousInvocations + 1)).superCreateItems() + bottomTabs.onSizeChanged(1920, 147, 0, 0) + Mockito.verify(bottomTabs, Mockito.times(numberOfPreviousInvocations + 2)).superCreateItems() + Mockito.`when`(bottomTabs.itemsCount).thenReturn(0) + bottomTabs.onSizeChanged(1080, 147, 0, 0) + Mockito.verify(bottomTabs, Mockito.times(numberOfPreviousInvocations + 2)).superCreateItems() + } + + @Test + fun mergeOptions_currentTabIndex() { + uut.ensureViewIsCreated() + Java6Assertions.assertThat(uut.selectedIndex).isZero + val options = Options() + options.bottomTabsOptions.currentTabIndex = Number(1) + uut.mergeOptions(options) + Java6Assertions.assertThat(uut.selectedIndex).isOne + Mockito.verify(eventEmitter, Mockito.times(0)).emitBottomTabSelected( + ArgumentMatchers.any( + Int::class.java + ), ArgumentMatchers.any(Int::class.java) + ) + } + + @Test + fun mergeOptions_drawBehind() { + Java6Assertions.assertThat(uut.getBottomInset(child1)).isEqualTo(uut.bottomTabs.height) + val o1 = Options() + o1.bottomTabsOptions.drawBehind = Bool(true) + child1.mergeOptions(o1) + Java6Assertions.assertThat(uut.getBottomInset(child1)).isEqualTo(0) + val o2 = Options() + o2.topBar.title.text = Text("Some text") + child1.mergeOptions(o1) + Java6Assertions.assertThat(uut.getBottomInset(child1)).isEqualTo(0) + } + + @Test + fun mergeOptions_drawBehind_stack() { + uut.ensureViewIsCreated() + uut.selectTab(3) + Java6Assertions.assertThat((stackChild.view.layoutParams as MarginLayoutParams).bottomMargin).isEqualTo( + bottomTabs.height + ) + val o1 = Options() + o1.bottomTabsOptions.drawBehind = Bool(true) + stackChild.mergeOptions(o1) + Java6Assertions.assertThat((stackChild.view.layoutParams as MarginLayoutParams).bottomMargin).isEqualTo(0) + } + + @Test + fun mergeOptions_mergesBottomTabOptions() { + val options = Options() + uut.mergeOptions(options) + Mockito.verify(bottomTabPresenter).mergeOptions(options) + } + + @Test + fun applyChildOptions_resolvedOptionsAreUsed() { + val childOptions = Options() + val pushedScreen = SimpleViewController(activity, childRegistry, "child4.1", childOptions) + disablePushAnimation(pushedScreen) + child4 = spyOnStack(pushedScreen) + tabs = ArrayList(listOf(child4)) + tabsAttacher = BottomTabsAttacher(tabs, presenter, Options.EMPTY) + initialOptions.bottomTabsOptions.currentTabIndex = Number(0) + val resolvedOptions = Options() + uut = object : BottomTabsController( + activity, + tabs, + childRegistry, + eventEmitter, + imageLoaderMock, + "uut", + initialOptions, + Presenter(activity, Options()), + tabsAttacher, + presenter, + BottomTabPresenter(activity, tabs, mock(), TypefaceLoaderMock(), Options()) + ) { + override fun resolveCurrentOptions(): Options { + return resolvedOptions + } + + override fun createBottomTabs(): BottomTabs { + return object : BottomTabs(activity) { + override fun createItems() {} + } + } + } + activity.setContentView(uut.view) + idleMainLooper() + Mockito.verify(presenter, Mockito.times(2)) + .applyChildOptions(eq(resolvedOptions), any()) + } + + @Test + fun child_mergeOptions_currentTabIndex() { + uut.ensureViewIsCreated() + Java6Assertions.assertThat(uut.selectedIndex).isZero + val options = Options() + options.bottomTabsOptions.currentTabIndex = Number(1) + child1.mergeOptions(options) + Java6Assertions.assertThat(uut.selectedIndex).isOne + } + + @Test + fun resolveCurrentOptions_returnsFirstTabIfInvokedBeforeViewIsCreated() { + uut = createBottomTabs() + Java6Assertions.assertThat(uut.currentChild).isEqualTo(tabs[0]) + } + + @Test + fun buttonPressInvokedOnCurrentTab() { + uut.ensureViewIsCreated() + uut.selectTab(4) + uut.sendOnNavigationButtonPressed("btn1") + Mockito.verify(child5, Mockito.times(1)).sendOnNavigationButtonPressed("btn1") + } + + @Test + fun push() { + uut.selectTab(3) + val stackChild2 = SimpleViewController(activity, childRegistry, "stackChild2", Options()) + disablePushAnimation(stackChild2) + TestUtils.hideBackButton(stackChild2) + Java6Assertions.assertThat(child4.size()).isEqualTo(1) + child4.push(stackChild2, CommandListenerAdapter()) + Java6Assertions.assertThat(child4.size()).isEqualTo(2) + } + + @Test + fun oneTimeOptionsAreAppliedOnce() { + val options = Options() + options.bottomTabsOptions.currentTabIndex = Number(1) + Java6Assertions.assertThat(uut.selectedIndex).isZero + uut.mergeOptions(options) + Java6Assertions.assertThat(uut.selectedIndex).isOne + Java6Assertions.assertThat(uut.options.bottomTabsOptions.currentTabIndex.hasValue()).isFalse + Java6Assertions.assertThat(uut.initialOptions.bottomTabsOptions.currentTabIndex.hasValue()).isFalse + } + + @Test + fun selectTab() { + uut.selectTab(1) + Mockito.verify(tabsAttacher).onTabSelected(tabs[1]) + } + + @Test + fun selectTab_onViewDidAppearIsInvokedAfterSelection() { + uut.selectTab(1) + Mockito.verify(child2).onViewDidAppear() + } + + @Test + fun creatingTabs_onViewDidAppearInvokedAfterInitialTabIndexSet() { + val options = Options.EMPTY.copy() + options.bottomTabsOptions.currentTabIndex = Number(1) + prepareViewsForTests(options.bottomTabsOptions) + idleMainLooper() + Mockito.verify(tabs[0], Mockito.times(0)).onViewDidAppear() + Mockito.verify(tabs[1], Mockito.times(1)).onViewDidAppear() + Mockito.verify(tabs[2], Mockito.times(0)).onViewDidAppear() + Mockito.verify(tabs[3], Mockito.times(0)).onViewDidAppear() + Mockito.verify(tabs[4], Mockito.times(0)).onViewDidAppear() + } + + @Test + fun topInset() { + Java6Assertions.assertThat(child1.topInset).isEqualTo(statusBarHeight) + Java6Assertions.assertThat(child2.topInset).isEqualTo(statusBarHeight) + child1.options.statusBar.drawBehind = Bool(true) + Java6Assertions.assertThat(child1.topInset).isEqualTo(0) + Java6Assertions.assertThat(child2.topInset).isEqualTo(statusBarHeight) + Java6Assertions.assertThat(stackChild.topInset).isEqualTo(statusBarHeight + child4.topBar.height) + } + + @Test + fun bottomInset_defaultOptionsAreTakenIntoAccount() { + val defaultOptions = Options() + defaultOptions.bottomTabsOptions.visible = Bool(false) + Java6Assertions.assertThat(uut.getBottomInset(child1)).isEqualTo(bottomTabs.height) + uut.setDefaultOptions(defaultOptions) + Java6Assertions.assertThat(uut.getBottomInset(child1)).isZero + } + + @Test + fun destroy() { + uut.destroy() + Mockito.verify(tabsAttacher).destroy() + } + + private fun prepareViewsForTests( + bottomTabsOptions: BottomTabsOptions = initialOptions.bottomTabsOptions, + options: Options = initialOptions, defaultOptions: Options = initialOptions + ) { + if(::uut.isInitialized){ + uut.destroy() + } +// ObjectUtils.perform(uut, { obj: BottomTabsController -> obj.destroy() }) + bottomTabs = Mockito.spy(object : BottomTabs(activity) { + override fun superCreateItems() {} + }) + bottomTabsContainer = Mockito.spy(BottomTabsContainer(activity, bottomTabs)) + createChildren() + tabs = mutableListOf(child1, child2, child3, child4, child5) + defaultOptions.bottomTabsOptions = bottomTabsOptions + presenter = Mockito.spy(BottomTabsPresenter(tabs, defaultOptions, BottomTabsAnimator())) + bottomTabPresenter = + Mockito.spy(BottomTabPresenter(activity, tabs, mock(), TypefaceLoaderMock(), defaultOptions)) + tabsAttacher = Mockito.spy(BottomTabsAttacher(tabs, presenter, defaultOptions)) + uut = createBottomTabs(options = options, defaultOptions = defaultOptions) + activity.setContentView(FakeParentController(activity, childRegistry, uut).view) + } + + private fun createChildren() { + child1 = Mockito.spy(SimpleViewController(activity, childRegistry, "child1", tabOptions)) + child2 = Mockito.spy(SimpleViewController(activity, childRegistry, "child2", tabOptions)) + child3 = Mockito.spy(SimpleViewController(activity, childRegistry, "child3", tabOptions)) + stackChild = Mockito.spy(SimpleViewController(activity, childRegistry, "stackChild", tabOptions)) + child4 = spyOnStack(stackChild) + child5 = Mockito.spy(SimpleViewController(activity, childRegistry, "child5", tabOptions)) + Mockito.`when`(child5.handleBack(any())).thenReturn(true) + } + + private fun spyOnStack(initialChild: ViewController<*>?): StackController { + val build = TestUtils.newStackController(activity) + .setInitialOptions(tabOptions) + .build() + val stack = Mockito.spy(build) + disablePushAnimation(initialChild) + stack.ensureViewIsCreated() + stack.push(initialChild, CommandListenerAdapter()) + return stack + } + + private fun createBottomTabs( + options: Options = initialOptions, + defaultOptions: Options = initialOptions + ): BottomTabsController { + return object : BottomTabsController( + activity, + tabs, + childRegistry, + eventEmitter, + imageLoaderMock, + "uut", + options, + Presenter(activity, defaultOptions), + tabsAttacher, + presenter, + bottomTabPresenter + ) { + override fun ensureViewIsCreated() { + super.ensureViewIsCreated() + uut.view.layout(0, 0, 1000, 1000) + } + + override fun createView(): BottomTabsLayout { + val view = super.createView() + this@BottomTabsControllerTest.bottomTabs.layoutParams.height = 100 + return view + } + + override fun createBottomTabsContainer(): BottomTabsContainer { + return this@BottomTabsControllerTest.bottomTabsContainer + } + + override fun createBottomTabs(): BottomTabs { + return this@BottomTabsControllerTest.bottomTabs + } + } + } + + private val statusBarHeight: Int + get() = getStatusBarHeight(activity) +} \ No newline at end of file diff --git a/lib/src/interfaces/Options.ts b/lib/src/interfaces/Options.ts index cb8997eab88..d98a0c499e1 100644 --- a/lib/src/interfaces/Options.ts +++ b/lib/src/interfaces/Options.ts @@ -414,6 +414,11 @@ export interface HardwareBackButtonOptions { * @default true */ popStackOnPress?: boolean; + + /** + * Controls hardware back button bottom tab selection behaviour + */ + bottomTabsOnPress?: 'exit' | 'first' | 'previous'; } export interface OptionsTopBarScrollEdgeAppearanceBackground { diff --git a/playground/src/screens/FirstBottomTabScreen.tsx b/playground/src/screens/FirstBottomTabScreen.tsx index 757f381b75c..48fd4add360 100644 --- a/playground/src/screens/FirstBottomTabScreen.tsx +++ b/playground/src/screens/FirstBottomTabScreen.tsx @@ -15,6 +15,7 @@ const { HIDE_TABS_BTN, SHOW_TABS_BTN, HIDE_TABS_PUSH_BTN, + FIRST_TAB_BAR_BUTTON, } = testIDs; export default class FirstBottomTabScreen extends React.Component { @@ -29,6 +30,7 @@ export default class FirstBottomTabScreen extends React.Component