Bug 1340929 - Don't scroll to a new tab opened from a link. r?sebastian
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
--- 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
@@ -101,17 +101,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());
+ }
}