blob: 4f87e864a57986bfed348be98e61b7e0b1c52cee [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.autofillservice.cts;
import static android.autofillservice.cts.Helper.SAVE_TIMEOUT_MS;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_ADDRESS;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_PASSWORD;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.res.Resources;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.util.Log;
import android.view.accessibility.AccessibilityWindowInfo;
import java.util.List;
/**
* Helper for UI-related needs.
*/
final class UiBot {
private static final String RESOURCE_ID_DATASET_PICKER = "autofill_dataset_picker";
private static final String RESOURCE_ID_SAVE_SNACKBAR = "autofill_save";
private static final String RESOURCE_ID_SAVE_TITLE = "autofill_save_title";
private static final String RESOURCE_ID_CONTEXT_MENUITEM = "floating_toolbar_menu_item_text";
private static final String RESOURCE_STRING_SAVE_TITLE = "autofill_save_title";
private static final String RESOURCE_STRING_SAVE_TITLE_WITH_TYPE =
"autofill_save_title_with_type";
private static final String RESOURCE_STRING_SAVE_TYPE_PASSWORD = "autofill_save_type_password";
private static final String RESOURCE_STRING_SAVE_TYPE_ADDRESS = "autofill_save_type_address";
private static final String RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD =
"autofill_save_type_credit_card";
private static final String RESOURCE_STRING_AUTOFILL = "autofill";
private static final String RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE =
"autofill_picker_accessibility_title";
private static final String TAG = "AutoFillCtsUiBot";
private final UiDevice mDevice;
private final long mTimeout;
private final String mPackageName;
private final UiAutomation mAutoman;
UiBot(Instrumentation instrumentation, long timeout) throws Exception {
mDevice = UiDevice.getInstance(instrumentation);
mTimeout = timeout;
mPackageName = instrumentation.getContext().getPackageName();
mAutoman = instrumentation.getUiAutomation();
}
/**
* Asserts the dataset chooser is not shown.
*/
void assertNoDatasets() {
final UiObject2 ui;
try {
ui = findDatasetPicker();
} catch (Throwable t) {
// TODO(b/33197203): use a more elegant check than catching the expection because it's
// not showing...
return;
}
throw new AssertionError("floating ui is shown: " + ui);
}
/**
* Asserts the dataset chooser is shown and contains the given datasets.
*
* @return the dataset picker object.
*/
UiObject2 assertDatasets(String...names) {
final UiObject2 picker = findDatasetPicker();
for (String name : names) {
final UiObject2 dataset = picker.findObject(By.text(name));
assertWithMessage("no dataset named %s", name).that(dataset).isNotNull();
}
return picker;
}
/**
* Selects a dataset that should be visible in the floating UI.
*/
void selectDataset(String name) {
final UiObject2 picker = findDatasetPicker();
selectDataset(picker, name);
}
/**
* Selects a dataset that should be visible in the floating UI.
*/
void selectDataset(UiObject2 picker, String name) {
final UiObject2 dataset = picker.findObject(By.text(name));
assertWithMessage("no dataset named %s", name).that(dataset).isNotNull();
dataset.click();
}
/**
* Selects a view by text.
*/
void selectByText(String name) {
Log.v(TAG, "selectByText(): " + name);
final UiObject2 dataset = waitForObject(By.text(name));
dataset.click();
}
/**
* Asserts a text is now shown.
*/
public void assertNotShownByText(String text) {
final UiObject2 uiObject = mDevice.findObject(By.text(text));
assertWithMessage(text).that(uiObject).isNull();
}
/**
* Checks if a View with a certain text exists.
*/
boolean hasViewWithText(String name) {
Log.v(TAG, "hasViewWithText(): " + name);
return mDevice.findObject(By.text(name)) != null;
}
/**
* Asserts the save snackbar is showing and returns it.
*/
UiObject2 assertSaveShowing(int type) {
return assertSaveShowing(type, null);
}
/**
* Asserts the save snackbar is not showing and returns it.
*/
void assertSaveNotShowing(int type) {
try {
assertSaveShowing(type);
} catch (Throwable t) {
// TODO(b/33197203): use a more elegant check than catching the expection because it's
// not showing (in which case it wouldn't need a type as parameter).
return;
}
throw new AssertionError("snack bar is showing");
}
UiObject2 assertSaveShowing(int type, String description) {
final UiObject2 snackbar = waitForObject(By.res("android", RESOURCE_ID_SAVE_SNACKBAR),
SAVE_TIMEOUT_MS);
final UiObject2 titleView = snackbar.findObject(By.res("android", RESOURCE_ID_SAVE_TITLE));
assertWithMessage("save title (%s)", RESOURCE_ID_SAVE_TITLE).that(titleView).isNotNull();
final String serviceLabel = InstrumentedAutoFillService.class.getSimpleName();
final String expectedTitle;
if (type == SAVE_DATA_TYPE_GENERIC) {
expectedTitle = getString(RESOURCE_STRING_SAVE_TITLE, serviceLabel);
} else {
final String typeResourceName;
switch (type) {
case SAVE_DATA_TYPE_PASSWORD:
typeResourceName = RESOURCE_STRING_SAVE_TYPE_PASSWORD;
break;
case SAVE_DATA_TYPE_ADDRESS:
typeResourceName = RESOURCE_STRING_SAVE_TYPE_ADDRESS;
break;
case SAVE_DATA_TYPE_CREDIT_CARD:
typeResourceName = RESOURCE_STRING_SAVE_TYPE_CREDIT_CARD;
break;
default:
throw new IllegalArgumentException("Unsupported type: " + type);
}
final String typeString = getString(typeResourceName);
expectedTitle = getString(RESOURCE_STRING_SAVE_TITLE_WITH_TYPE, typeString,
serviceLabel);
}
final String actualTitle = titleView.getText();
Log.d(TAG, "save title: " + actualTitle);
assertThat(actualTitle).isEqualTo(expectedTitle);
if (description != null) {
final UiObject2 saveSubTitle = snackbar.findObject(By.text(description));
assertWithMessage("save subtitle(%s)", description).that(saveSubTitle).isNotNull();
}
return snackbar;
}
/**
* Taps an option in the save snackbar.
*
* @param type expected type of save info.
* @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
*/
void saveForAutofill(int type, boolean yesDoIt) {
final UiObject2 saveSnackBar = assertSaveShowing(type, null);
saveForAutofill(saveSnackBar, yesDoIt);
}
/**
* Taps an option in the save snackbar.
*
* @param saveSnackBar Save snackbar, typically obtained through
* {@link #assertSaveShowing(int)}.
* @param yesDoIt {@code true} for 'YES', {@code false} for 'NO THANKS'.
*/
void saveForAutofill(UiObject2 saveSnackBar, boolean yesDoIt) {
final String id = yesDoIt ? "autofill_save_yes" : "autofill_save_no";
final UiObject2 button = saveSnackBar.findObject(By.res("android", id));
assertWithMessage("save button (%s)", id).that(button).isNotNull();
button.click();
}
/**
* Gets the AUTOFILL contextual menu by long pressing a text field.
*
* @param id resource id of the field.
*/
UiObject2 getAutofillMenuOption(String id) {
final UiObject2 field = waitForObject(By.res(mPackageName, id));
// TODO(b/33197203, b/33802548): figure out why obj.longClick() doesn't always work
field.click(3000);
final List<UiObject2> menuItems = waitForObjects(
By.res("android", RESOURCE_ID_CONTEXT_MENUITEM));
final String expectedText = getString(RESOURCE_STRING_AUTOFILL);
final StringBuffer menuNames = new StringBuffer();
for (UiObject2 menuItem : menuItems) {
final String menuName = menuItem.getText();
if (menuName.equalsIgnoreCase(expectedText)) {
return menuItem;
}
menuNames.append("'").append(menuName).append("' ");
}
throw new AssertionError("no '" + expectedText + "' on " + menuNames);
}
/**
* Gets a string from the Android resources.
*/
private String getString(String id) {
final Resources resources = InstrumentationRegistry.getContext().getResources();
final int stringId = resources.getIdentifier(id, "string", "android");
return resources.getString(stringId);
}
/**
* Gets a string from the Android resources.
*/
private String getString(String id, Object... formatArgs) {
final Resources resources = InstrumentationRegistry.getContext().getResources();
final int stringId = resources.getIdentifier(id, "string", "android");
return resources.getString(stringId, formatArgs);
}
/**
* Waits for and returns an object.
*
* @param selector {@link BySelector} that identifies the object.
*/
private UiObject2 waitForObject(BySelector selector) {
return waitForObject(selector, mTimeout);
}
/**
* Waits for and returns an object.
*
* @param selector {@link BySelector} that identifies the object.
* @param timeout timeout in ms
*/
private UiObject2 waitForObject(BySelector selector, long timeout) {
// NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
final int maxTries = 5;
final long napTime = timeout / maxTries;
for (int i = 1; i <= maxTries; i++) {
final UiObject2 uiObject = mDevice.findObject(selector);
if (uiObject != null) {
return uiObject;
}
SystemClock.sleep(napTime);
}
throw new AssertionError("Object with selector " + selector + " not found in "
+ mTimeout + " ms");
}
/**
* Waits for and returns a list of objects.
*
* @param selector {@link BySelector} that identifies the object.
*/
private List<UiObject2> waitForObjects(BySelector selector) {
return waitForObjects(selector, mTimeout);
}
/**
* Waits for and returns a list of objects.
*
* @param selector {@link BySelector} that identifies the object.
* @param timeout timeout in ms
*/
private List<UiObject2> waitForObjects(BySelector selector, long timeout) {
// NOTE: mDevice.wait does not work for the save snackbar, so we need a polling approach.
final int maxTries = 5;
final long napTime = timeout / maxTries;
for (int i = 1; i <= maxTries; i++) {
final List<UiObject2> uiObjects = mDevice.findObjects(selector);
if (uiObjects != null && !uiObjects.isEmpty()) {
return uiObjects;
}
SystemClock.sleep(napTime);
}
throw new AssertionError("Objects with selector " + selector + " not found in "
+ mTimeout + " ms");
}
private UiObject2 findDatasetPicker() {
final UiObject2 picker = waitForObject(By.res("android", RESOURCE_ID_DATASET_PICKER));
final String expectedTitle = getString(RESOURCE_STRING_DATASET_PICKER_ACCESSIBILITY_TITLE);
assertAccessibilityTitle(picker, expectedTitle);
return picker;
}
/**
* Asserts a given object has the expected accessibility title.
*/
private void assertAccessibilityTitle(UiObject2 object, String expectedTitle) {
// TODO: ideally it should get the AccessibilityWindowInfo from the object, but UiAutomator
// does not expose that.
for (AccessibilityWindowInfo window : mAutoman.getWindows()) {
final CharSequence title = window.getTitle();
if (title != null && title.toString().equals(expectedTitle)) {
return;
}
}
throw new AssertionError("Title (" + expectedTitle + ") not found for " + object);
}
}