0

[Android] Create ViewElement#createSettleCondition()

After closing the soft keyboard, wait for views of the
NewTabGroupDialogFacility to settle.

Re-enable TabGroupDialogPTTest which is likely flaky due to the
dialog moving after the soft keyboard closes, while we are generating an input event for it.

Bug: 391888241
Change-Id: If2fe0dd6b791bba9ee3042899a2bcfb2898d9309
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6190496
Reviewed-by: Sky Malice <skym@chromium.org>
Commit-Queue: Henrique Nakashima <hnakashima@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1411622}
This commit is contained in:
Henrique Nakashima
2025-01-27 07:46:09 -08:00
committed by Chromium LUCI CQ
parent b924ba930e
commit 345b51b68b
5 changed files with 102 additions and 40 deletions
base/test/android/javatests/src/org/chromium/base/test/transit
chrome
android
features
tab_ui
javatests
src
org
chromium
chrome
browser
tasks
test
android
javatests

@ -45,6 +45,11 @@ public class ViewConditions {
private final Matcher<View> mMatcher;
private final Options mOptions;
private View mViewMatched;
private int mPreviousViewX = Integer.MIN_VALUE;
private int mPreviousViewY = Integer.MIN_VALUE;
private int mPreviousViewWidth = Integer.MIN_VALUE;
private int mPreviousViewHeight = Integer.MIN_VALUE;
private long mLastChangeMs = -1;
public DisplayedCondition(Matcher<View> matcher, Options options) {
super(/* isRunOnUiThread= */ false);
@ -61,6 +66,9 @@ public class ViewConditions {
.append(" (>= ")
.append(mOptions.mDisplayedPercentageRequired)
.append("% displayed");
if (mOptions.mSettleTimeMs > 0) {
description.append(", settled for ").append(mOptions.mSettleTimeMs).append("ms");
}
if (mOptions.mExpectEnabled) {
description.append(", enabled");
}
@ -143,6 +151,7 @@ public class ViewConditions {
messages.add(String.format("%d%% displayed", portion.mPercentage));
}
}
if (mOptions.mExpectEnabled) {
if (!mViewMatched.isEnabled()) {
fulfilled = false;
@ -155,6 +164,34 @@ public class ViewConditions {
}
}
if (mOptions.mSettleTimeMs > 0) {
long nowMs = System.currentTimeMillis();
int[] locationOnScreen = new int[2];
mViewMatched.getLocationOnScreen(locationOnScreen);
int newX = locationOnScreen[0];
int newY = locationOnScreen[1];
int newWidth = view.getWidth();
int newHeight = view.getHeight();
if (mPreviousViewX != newX
|| mPreviousViewY != newY
|| mPreviousViewWidth != newWidth
|| mPreviousViewHeight != newHeight) {
mPreviousViewX = newX;
mPreviousViewY = newY;
mPreviousViewWidth = newWidth;
mPreviousViewHeight = newHeight;
mLastChangeMs = nowMs;
}
long timeSinceMoveMs = nowMs - mLastChangeMs;
if (timeSinceMoveMs < mOptions.mSettleTimeMs) {
fulfilled = false;
messages.add("Not settled for " + mOptions.mSettleTimeMs + "ms");
} else {
messages.add("Settled for " + mOptions.mSettleTimeMs + "ms");
}
}
String message = String.join("; ", messages);
if (fulfilled) {
return fulfilled(message).withResult(mViewMatched);
@ -184,6 +221,7 @@ public class ViewConditions {
boolean mExpectEnabled = true;
boolean mExpectDisabled;
int mDisplayedPercentageRequired = ViewElement.MIN_DISPLAYED_PERCENT;
int mSettleTimeMs;
private Options() {}
@ -209,6 +247,12 @@ public class ViewConditions {
mDisplayedPercentageRequired = displayedPercentageRequired;
return this;
}
/** How long the View's rect needs to be unchanged. */
public Builder withSettleTimeMs(int settleTimeMs) {
mSettleTimeMs = settleTimeMs;
return this;
}
}
}
}

@ -60,6 +60,23 @@ public class ViewElement extends Element<View> {
.withExpectEnabled(mOptions.mExpectEnabled)
.withExpectDisabled(mOptions.mExpectDisabled)
.withDisplayingAtLeast(mOptions.mDisplayedPercentageRequired)
.withSettleTimeMs(mOptions.mInitialSettleTimeMs)
.build();
return new DisplayedCondition(viewMatcher, conditionOptions);
}
/**
* Create a {@link DisplayedCondition} like the enter Condition, but also waiting for the View
* to settle (no changes to its rect coordinates) for 1 second.
*/
public ConditionWithResult<View> createSettleCondition() {
Matcher<View> viewMatcher = mViewSpec.getViewMatcher();
DisplayedCondition.Options conditionOptions =
DisplayedCondition.newOptions()
.withExpectEnabled(mOptions.mExpectEnabled)
.withExpectDisabled(mOptions.mExpectDisabled)
.withDisplayingAtLeast(mOptions.mDisplayedPercentageRequired)
.withSettleTimeMs(1000)
.build();
return new DisplayedCondition(viewMatcher, conditionOptions);
}
@ -80,7 +97,8 @@ public class ViewElement extends Element<View> {
protected boolean mExpectEnabled = true;
protected boolean mExpectDisabled;
protected String mElementId;
protected Integer mDisplayedPercentageRequired = ViewElement.MIN_DISPLAYED_PERCENT;
protected int mDisplayedPercentageRequired = ViewElement.MIN_DISPLAYED_PERCENT;
protected int mInitialSettleTimeMs;
protected Options() {}
@ -132,6 +150,12 @@ public class ViewElement extends Element<View> {
mDisplayedPercentageRequired = percentage;
return this;
}
/** Waits for the View's rect to stop moving. */
public Builder initialSettleTime(int settleTimeMs) {
mInitialSettleTimeMs = settleTimeMs;
return this;
}
}
}

@ -15,7 +15,6 @@ import org.junit.runner.RunWith;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
@ -48,7 +47,6 @@ public class TabGroupDialogPTTest {
@Test
@MediumTest
@DisabledTest(message = "crbug.com/381595663")
public void testNewTabCreation() {
WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
WebPageStation pageStation =
@ -67,7 +65,6 @@ public class TabGroupDialogPTTest {
@Test
@MediumTest
@DisabledTest(message = "crbug.com/381595663")
public void testIncognitoNewTabCreation() {
WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
WebPageStation pageStation =
@ -86,7 +83,6 @@ public class TabGroupDialogPTTest {
@Test
@MediumTest
@DisabledTest(message = "crbug.com/381595663")
public void testTabGroupNameChange() {
WebPageStation firstPage = mInitialStateRule.startOnBlankPage();
WebPageStation pageStation =

@ -9,6 +9,8 @@ import androidx.test.espresso.Espresso;
import org.chromium.base.test.transit.Elements;
import org.chromium.base.test.transit.Facility;
import org.chromium.base.test.transit.Station;
import org.chromium.base.test.transit.Transition;
import org.chromium.base.test.transit.ViewElement;
import org.chromium.ui.test.transit.SoftKeyboardElement;
/** Represents the soft keyboard shown, expecting it to hide after exiting the Facility. */
@ -21,7 +23,15 @@ public class SoftKeyboardFacility extends Facility<Station<?>> {
elements.declareElement(new SoftKeyboardElement(mHostStation.getActivityElement()));
}
public void close() {
/**
* Close the soft keyboard and wait for the passed ViewElements to stop moving after the
* relayout.
*
* <p>If it was expected to not be shown, just ensure that and exit this Facility.
*
* @param viewElementsToSettle the ViewElements to wait to stop moving
*/
public void close(ViewElement... viewElementsToSettle) {
assertSuppliersCanBeUsed();
if (mSoftKeyboardElement.get()) {
@ -29,7 +39,11 @@ public class SoftKeyboardFacility extends Facility<Station<?>> {
// If this fails, the keyboard was closed before, but not by this facility.
recheckActiveConditions();
mHostStation.exitFacilitySync(this, Espresso::pressBack);
Transition.TransitionOptions.Builder options = Transition.newOptions();
for (ViewElement viewElement : viewElementsToSettle) {
options.withCondition(viewElement.createSettleCondition());
}
mHostStation.exitFacilitySync(this, options.build(), Espresso::pressBack);
} else {
// Keyboard was not expected to be shown
mHostStation.exitFacilitySync(this, /* trigger= */ null);

@ -60,6 +60,7 @@ public class NewTabGroupDialogFacility extends Facility<TabSwitcherStation> {
private final @Nullable @TabGroupColorId Integer mSelectedColor;
private final SoftKeyboardFacility mSoftKeyboard;
private ViewSpec mTitleInputSpec;
private ViewElement mDialog;
/** Constructor. Expects no particular title or selected color. */
public NewTabGroupDialogFacility(
@ -85,7 +86,7 @@ public class NewTabGroupDialogFacility extends Facility<TabSwitcherStation> {
@Override
public void declareElements(Elements.Builder elements) {
elements.declareView(DIALOG, ViewElement.displayingAtLeastOption(80));
mDialog = elements.declareView(DIALOG, ViewElement.displayingAtLeastOption(80));
elements.declareView(DIALOG_TITLE);
String inputElementId = "Tab group title input showing " + mTitle;
@ -132,14 +133,7 @@ public class NewTabGroupDialogFacility extends Facility<TabSwitcherStation> {
// An empty name causes warning text to show up which could push the color picker container
// out of view for small screen devices, so dismiss the keyboard.
if (newTabGroupName.isEmpty()) {
if (mSoftKeyboard.getPhase() == Phase.ACTIVE) {
mSoftKeyboard.close();
} else if (mSoftKeyboard.getPhase() == Phase.FINISHED) {
// Do nothing as the soft keyboard has already been closed
} else {
throw new IllegalArgumentException(
"SoftKeyboardFacility is in phase " + mSoftKeyboard.getPhase());
}
ensureSoftKeyboardClosed();
}
return mHostStation.swapFacilitySync(
@ -159,14 +153,7 @@ public class NewTabGroupDialogFacility extends Facility<TabSwitcherStation> {
/** Press "Done" to confirm the tab group name and color. */
public TabSwitcherGroupCardFacility pressDone() {
if (mSoftKeyboard.getPhase() == Phase.ACTIVE) {
mSoftKeyboard.close();
} else if (mSoftKeyboard.getPhase() == Phase.FINISHED) {
// Do nothing as the soft keyboard has already been closed
} else {
throw new IllegalArgumentException(
"SoftKeyboardFacility is in phase " + mSoftKeyboard.getPhase());
}
ensureSoftKeyboardClosed();
// The reason we can pass an expected card index is because the tab group has already been
// created.
@ -180,14 +167,7 @@ public class NewTabGroupDialogFacility extends Facility<TabSwitcherStation> {
/** Press "Done" to confirm the tab group name and color, but no-op from an invalid title. */
public NewTabGroupDialogFacility pressDoneWithInvalidTitle() {
if (mSoftKeyboard.getPhase() == Phase.ACTIVE) {
mSoftKeyboard.close();
} else if (mSoftKeyboard.getPhase() == Phase.FINISHED) {
// Do nothing as the soft keyboard has already been closed
} else {
throw new IllegalArgumentException(
"SoftKeyboardFacility is in phase " + mSoftKeyboard.getPhase());
}
ensureSoftKeyboardClosed();
return mHostStation.swapFacilitySync(
this,
@ -199,14 +179,7 @@ public class NewTabGroupDialogFacility extends Facility<TabSwitcherStation> {
/** Press the system backpress to confirm the tab group name and color. */
public TabSwitcherGroupCardFacility pressBack() {
if (mSoftKeyboard.getPhase() == Phase.ACTIVE) {
mSoftKeyboard.close();
} else if (mSoftKeyboard.getPhase() == Phase.FINISHED) {
// Do nothing as the soft keyboard has already been closed
} else {
throw new IllegalArgumentException(
"SoftKeyboardFacility is in phase " + mSoftKeyboard.getPhase());
}
ensureSoftKeyboardClosed();
// The reason we can pass an expected card index is because the tab group has already been
// created.
@ -219,4 +192,15 @@ public class NewTabGroupDialogFacility extends Facility<TabSwitcherStation> {
Espresso.pressBack();
});
}
private void ensureSoftKeyboardClosed() {
if (mSoftKeyboard.getPhase() == Phase.ACTIVE) {
mSoftKeyboard.close(mDialog);
} else if (mSoftKeyboard.getPhase() == Phase.FINISHED) {
// Do nothing as the soft keyboard has already been closed
} else {
throw new IllegalArgumentException(
"SoftKeyboardFacility is in phase " + mSoftKeyboard.getPhase());
}
}
}