Bug 1340929 - Don't scroll to a new tab opened from a link. r=sebastian, a=lizzard
authorTom Klein <twointofive@gmail.com>
Wed, 22 Feb 2017 21:54:11 -0600
changeset 378885 ccfe0752e919fef62cb476e3fd16fe13db49b96d
parent 378884 02bbbf7e0b0a9440ece4d5955fde099be21e74f8
child 378886 3dadcef3629a9e3cce5a47116cad84591220c087
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian, lizzard
bugs1340929
milestone53.0a2
Bug 1340929 - Don't scroll to a new tab opened from a link. r=sebastian, a=lizzard We used to scroll in addTab to make sure a new tab created by a close-tab-undo at the start or the end of the list was made visible instead of staying where it was created off the edge. We're now taking care of that in selectTab (where it should have stayed in the first place), where the select in that case occurs between the time when the new tab is added to the adapter and when the layout gets updated. In the case where the new tab is at the start, that means the check 'position < layoutManager.findFirstCompletelyVisibleItemPosition()' in selectTab reads '0 < 0', which fails (which is why we need the new check for 'position == 0'), but the check 'position > layoutManager.findLastCompletelyVisibleItemPosition()' for a tab added at the end reads 'new_lengh -1 > old_length - 1' which already passes, so we don't need a special case for undo-tab-close adds at the end in selectTab. Tabs added at the end by a normal "create new tab" still scroll for the same reason. Robotium was confused by the duplicate 'add_tab' ids from the tab strip and the tabs panel, so I renamed one of them. Also note that the 'getTabId' added to TabStripItemView for testing already exists on TabLayoutItemView, but the two classes don't share a common base. MozReview-Commit-ID: BzG2r8BSs90
mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
mobile/android/base/resources/layout/tab_strip_inner.xml
mobile/android/tests/browser/robocop/robocop.ini
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTabStrip.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTabStripPrivacyMode.java
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStrip.java
@@ -39,17 +39,17 @@ public class TabStrip extends ThemedLine
 
     public TabStrip(Context context, AttributeSet attrs) {
         super(context, attrs);
         setOrientation(HORIZONTAL);
 
         LayoutInflater.from(context).inflate(R.layout.tab_strip_inner, this);
         tabStripView = (TabStripView) findViewById(R.id.tab_strip);
 
-        addTabButton = (ThemedImageButton) findViewById(R.id.add_tab);
+        addTabButton = (ThemedImageButton) findViewById(R.id.tablet_add_tab);
         addTabButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 final Tabs tabs = Tabs.getInstance();
                 if (isPrivateMode()) {
                     tabs.addPrivateTab();
                 } else {
                     tabs.addTab();
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripItemView.java
@@ -5,16 +5,17 @@
 
 package org.mozilla.gecko.tabs;
 
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.widget.ResizablePathDrawable;
 import org.mozilla.gecko.widget.ResizablePathDrawable.NonScaledPathShape;
 import org.mozilla.gecko.widget.themed.ThemedImageButton;
 import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
 import org.mozilla.gecko.widget.themed.ThemedTextView;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -103,16 +104,21 @@ public class TabStripItemView extends Th
                 }
 
                 final Tabs tabs = Tabs.getInstance();
                 tabs.closeTab(tabs.getTab(id), true);
             }
         });
     }
 
+    @RobocopTarget
+    public int getTabId() {
+        return id;
+    }
+
     @Override
     protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
         super.onSizeChanged(width, height, oldWidth, oldHeight);
 
         // Queue a tab region update in the next draw() call. We don't
         // update it immediately here because we need the new path from
         // the background drawable to be updated first.
         tabRegionNeedsUpdate = true;
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabStripView.java
@@ -90,21 +90,16 @@ public class TabStripView extends Recycl
     }
 
     /* package */ void addTab(Tab tab, int position) {
         if (tab.isPrivate() != isPrivate) {
             return;
         }
 
         adapter.addTab(tab, position);
-        position = position == -1 ? adapter.getItemCount() - 1 : position;
-        if (position == 0 || position == adapter.getItemCount() - 1) {
-            // A new first or last tab gets added off screen, so scroll to make it visible.
-            scrollToPosition(position);
-        }
     }
 
     /* package */ void removeTab(Tab tab) {
         adapter.removeTab(tab);
     }
 
     /* package */ void selectTab(Tab tab) {
         if (tab.isPrivate() != isPrivate) {
@@ -112,23 +107,25 @@ public class TabStripView extends Recycl
             refreshTabs();
             return;
         }
         final int position = adapter.getPositionForTab(tab);
         if (position == -1) {
             return;
         }
         // scrollToPosition sometimes needlessly scrolls even when position is already in view, so
-        // don't scrollToPosition unless necessary.
+        // don't scrollToPosition unless necessary.  (The position == 0 case is needed to scroll
+        // tab 0 into view when it's added back after a close undo (in which case the adapter and
+        // layout may be temporarily out of sync, so the rest of the if doesn't work).)
         final LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
         if (position < layoutManager.findFirstCompletelyVisibleItemPosition() ||
-                position > layoutManager.findLastCompletelyVisibleItemPosition()) {
+                position > layoutManager.findLastCompletelyVisibleItemPosition() ||
+                position == 0) {
             scrollToPosition(position);
         }
-
     }
 
     /* package */ void updateTab(Tab tab) {
         adapter.notifyTabChanged(tab);
     }
 
     public int getPositionForSelectedTab() {
         return adapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
--- a/mobile/android/base/resources/layout/tab_strip_inner.xml
+++ b/mobile/android/base/resources/layout/tab_strip_inner.xml
@@ -11,17 +11,17 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_weight="1"
         android:paddingTop="8dp"/>
 
     <!-- The right margin creates a "dead area" on the right side of the button
          which we compensate for with a touch delegate. See TabStrip -->
     <org.mozilla.gecko.widget.themed.ThemedImageButton
-        android:id="@+id/add_tab"
+        android:id="@+id/tablet_add_tab"
         style="@style/UrlBar.ImageButton"
         android:layout_width="@dimen/tablet_tab_strip_height"
         android:src="@drawable/tab_new"
         gecko:drawableTintList="@color/tab_new_tab_strip_colors"
         android:contentDescription="@string/new_tab"
         android:layout_marginRight="9dp"
         android:background="@drawable/tab_strip_button"/>
 
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -102,17 +102,17 @@ skip-if = android_version == "18"
 [src/org/mozilla/gecko/tests/testInputConnection.java]
 [src/org/mozilla/gecko/tests/testJavascriptBridge.java]
 [src/org/mozilla/gecko/tests/testReaderCacheMigration.java]
 [src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java]
 [src/org/mozilla/gecko/tests/testNativeCrypto.java]
 [src/org/mozilla/gecko/tests/testReaderModeTitle.java]
 [src/org/mozilla/gecko/tests/testSessionHistory.java]
 [src/org/mozilla/gecko/tests/testStateWhileLoading.java]
-[src/org/mozilla/gecko/tests/testTabStripPrivacyMode.java]
+[src/org/mozilla/gecko/tests/testTabStrip.java]
 [src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java]
 
 [src/org/mozilla/gecko/tests/testAccessibleCarets.java]
 
 # testStumblerSetting disabled on Android 4.3, bug 1145846
 [src/org/mozilla/gecko/tests/testStumblerSetting.java]
 skip-if = android_version == "18"
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java
@@ -1,15 +1,18 @@
 package org.mozilla.gecko.tests.components;
 
 import android.view.View;
 import android.support.v7.widget.RecyclerView;
 
 import com.robotium.solo.Condition;
 
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.RobocopUtils;
+import org.mozilla.gecko.tabs.TabStripItemView;
 import org.mozilla.gecko.tests.UITestContext;
 import org.mozilla.gecko.tests.helpers.DeviceHelper;
 import org.mozilla.gecko.tests.helpers.WaitHelper;
 
 import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
 
 /**
  * A class representing any interactions that take place on the tablet tab strip.
@@ -22,44 +25,127 @@ public class TabStripComponent extends B
         super(testContext);
     }
 
     public TabStripComponent assertTabCount(int count) {
         fAssertEquals("The tab strip tab count is " + count, count, getTabStripView().getAdapter().getItemCount());
         return this;
     }
 
+    /**
+     * Scroll the tab at {@code index} into view and click on it, where {@code index} is with
+     * respect to the start of the tabs list (and not just relative to the tabs visible on screen).
+     */
     public void switchToTab(int index) {
         // The tab strip is only available on tablets
         DeviceHelper.assertIsTablet();
 
         View tabView = waitForTabView(index);
         fAssertNotNull(String.format("Tab at index %d is not null", index), tabView);
 
         mSolo.clickOnView(tabView);
     }
 
-    /**
-     * Note: this currently only supports the case where the tab strip visible tabs start at tab 0
-     * and the tab at {@code index} is visible in the tab strip.
-     */
-    private View waitForTabView(final int index) {
-        final RecyclerView tabStrip = getTabStripView();
-        final View[] tabView = new View[1];
+    public void clickNewTabButton() {
+        final int preNewTabCount = getTabCount();
+
+        mSolo.clickOnView(mSolo.getView(R.id.tablet_add_tab));
 
-        WaitHelper.waitFor(String.format("Tab at index %d to be visible", index), new Condition() {
+        waitForNewTab(preNewTabCount);
+    }
+
+    public void waitForNewTab(final int tabCountBeforeNewTab) {
+        WaitHelper.waitFor("new tab", new Condition() {
             @Override
             public boolean isSatisfied() {
-                return (tabView[0] = tabStrip.getChildAt(index)) != null;
+                return getTabCount() == tabCountBeforeNewTab + 1;
+            }
+        });
+    }
+
+    /**
+     * Add tabs until scrolling is required to view all tabs, and then add at least one more.
+     */
+    public void fillStripWithTabs() {
+        if (getTabCount() > getTabStripView().getChildCount() + 1) {
+            // We're already full and then some.
+            return;
+        }
+
+        waitForTabView(0);
+        final int firstId = getTabViewAtVisualIndex(0).getTabId();
+
+        while (true) {
+            clickNewTabButton();
+            if (getTabViewAtVisualIndex(0).getTabId() != firstId) {
+                break;
+            }
+        }
+
+        // Add an extra to be really convincing.
+        clickNewTabButton();
+    }
+
+    /**
+     * @param index the *visual* index of the tab view to return (the first visible tab is index 0).
+     * @return the tab view at the given visual index.
+     */
+    public TabStripItemView getTabViewAtVisualIndex(int index) {
+        final RecyclerView tabStripView = getTabStripView();
+        fAssertTrue("The tab at index " + index + " is visible", index >= 0 && index < tabStripView.getChildCount());
+        return (TabStripItemView) tabStripView.getChildAt(index);
+    }
+
+
+    public int getTabCount() {
+        return getTabStripView().getAdapter().getItemCount();
+    }
+
+    private View waitForTabView(final int index) {
+        final View[] childView = { null };
+
+        final RecyclerView tabStrip = getTabStripView();
+
+        RobocopUtils.runOnUiThreadSync(mTestContext.getActivity(), new Runnable() {
+            @Override
+            public void run() {
+                tabStrip.scrollToPosition(index);
+                // The selection isn't updated synchronously; posting a runnable to the view's queue
+                // guarantees we'll run after the layout pass.
+                tabStrip.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        final RecyclerView.ViewHolder itemViewHolder = tabStrip.findViewHolderForLayoutPosition(index);
+                        childView[0] = itemViewHolder == null ? null : itemViewHolder.itemView;
+                    }
+                });
             }
         });
 
-        return tabView[0];
+        WaitHelper.waitFor("tab at " + index + " to scroll into view",
+                new Condition() {
+                    @Override
+                    public boolean isSatisfied() {
+                        return childView[0] != null;
+                    }
+                });
+
+        fAssertNotNull("Item at index " + index + " exists", childView[0]);
+
+        return childView[0];
+    }
+
+    /**
+     * You should generally use {@link #getTabStripView()} instead unless you're sure the view
+     * exists; this version may return {@code null}.
+     */
+    private RecyclerView maybeGetTabStripView() {
+        return (RecyclerView) mSolo.getView("tab_strip");
     }
 
     private RecyclerView getTabStripView() {
-        RecyclerView tabStrip = (RecyclerView) mSolo.getView("tab_strip");
+        RecyclerView tabStrip = maybeGetTabStripView();
 
         fAssertNotNull("Tab strip is not null", tabStrip);
 
         return tabStrip;
     }
 }
rename from mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTabStripPrivacyMode.java
rename to mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTabStrip.java
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTabStripPrivacyMode.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTabStrip.java
@@ -1,29 +1,38 @@
 package org.mozilla.gecko.tests;
 
 import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.tests.helpers.DeviceHelper;
 import org.mozilla.gecko.tests.helpers.GeckoClickHelper;
 import org.mozilla.gecko.tests.helpers.GeckoHelper;
 import org.mozilla.gecko.tests.helpers.NavigationHelper;
 import org.mozilla.gecko.tests.helpers.WaitHelper;
 
-/**
- * Make sure that a private tab opened while the tab strip is in normal mode does not get added to
- * the tab strip (bug 1339066).
- */
-public class testTabStripPrivacyMode extends UITest {
-    public void testTabStripPrivacyMode() {
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
+
+public class testTabStrip extends UITest {
+    public void testTabStrip() {
         if (!DeviceHelper.isTablet()) {
             return;
         }
 
         GeckoHelper.blockForReady();
 
+        testOpenPrivateTabInNormalMode();
+        // This test depends on ROBOCOP_BIG_LINK_URL being loaded in tab 0 from the first test.
+        testNewNormalTabScroll();
+    }
+
+    /**
+     * Make sure that a private tab opened while the tab strip is in normal mode does not get added
+     * to the tab strip (bug 1339066).
+    */
+    private void testOpenPrivateTabInNormalMode() {
         final String normalModeUrl = mStringHelper.ROBOCOP_BIG_LINK_URL;
         NavigationHelper.enterAndLoadUrl(normalModeUrl);
 
         final Actions.EventExpecter titleExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Content:DOMTitleChanged");
         GeckoClickHelper.openCentralizedLinkInNewPrivateTab();
 
         titleExpecter.blockForEvent();
         titleExpecter.unregisterListener();
@@ -34,9 +43,28 @@ public class testTabStripPrivacyMode ext
         mSolo.sleep(250);
 
         // Now make sure there's still only one tab in the tab strip, and that it's still the normal
         // mode tab.
 
         mTabStrip.assertTabCount(1);
         mToolbar.assertTitle(normalModeUrl);
     }
+
+    /**
+     * Test that we do *not* scroll to a new normal tab opened from a link in a page (bug 1340929).
+     * Assumes ROBOCOP_BIG_LINK_URL is loaded in tab 0.
+     */
+    private void testNewNormalTabScroll() {
+        mTabStrip.fillStripWithTabs();
+        mTabStrip.switchToTab(0);
+
+        final int tabZeroId = mTabStrip.getTabViewAtVisualIndex(0).getTabId();
+
+        final int tabCountBeforeNewTab = mTabStrip.getTabCount();
+        GeckoClickHelper.openCentralizedLinkInNewTab();
+        mTabStrip.waitForNewTab(tabCountBeforeNewTab);
+
+        // If we scrolled to the new tab then the first tab visible on screen will no longer be the
+        // tabs list tab 0.
+        fAssertEquals("Current first tab is tabs list first tab", tabZeroId, mTabStrip.getTabViewAtVisualIndex(0).getTabId());
+    }
  }