Robotests for BluetoothPairingDialog

Created some tests to protect some basic bluetooth
pairing dialogs features from regressing. Most of the
tests in this CL ensure that the view is properly
created and that it is properly updating the
associated controller when a relevant action occurs.

Test: make RunSettingsRoboTests
Bug: 32180625
Change-Id: I2f4103a39ffced52353712f952e8ff3d26590169
This commit is contained in:
Salvador Martinez
2016-10-24 15:51:58 -07:00
parent 78c4f30ae2
commit f492c28cc1
4 changed files with 452 additions and 18 deletions

View File

@@ -188,7 +188,7 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
*
* @return - The message ID to show the user.
*/
public int getDeviceVariantMessageID() {
public int getDeviceVariantMessageId() {
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
case BluetoothDevice.PAIRING_VARIANT_PIN:
@@ -198,7 +198,7 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
return R.string.bluetooth_enter_passkey_other_device;
default:
return -1;
return INVALID_DIALOG_TYPE;
}
}
@@ -208,7 +208,7 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
*
* @return - The message ID to show the user.
*/
public int getDeviceVariantMessageHint() {
public int getDeviceVariantMessageHintId() {
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
return R.string.bluetooth_pin_values_hint_16_digits;
@@ -218,7 +218,7 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
return R.string.bluetooth_pin_values_hint;
default:
return -1;
return INVALID_DIALOG_TYPE;
}
}

View File

@@ -45,10 +45,9 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
private static final String TAG = "BTPairingDialogFragment";
private AlertDialog.Builder mBuilder;
private BluetoothPairingController mPairingController;
private AlertDialog mDialog;
private BluetoothPairingController mPairingController;
private EditText mPairingView;
/**
* The interface we expect a listener to implement. Typically this should be done by
* the controller.
@@ -105,12 +104,26 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
return MetricsEvent.BLUETOOTH_DIALOG_FRAGMENT;
}
/**
* Used in testing to get a reference to the dialog.
* @return - The fragments current dialog
*/
protected AlertDialog getmDialog() {
return mDialog;
}
/**
* Sets the controller that the fragment should use. this method MUST be called
* before you try to show the dialog or an error will be thrown. An implementation
* of a pairing controller can be found at {@link BluetoothPairingController}.
* of a pairing controller can be found at {@link BluetoothPairingController}. A
* controller may not be substituted once it is assigned. Forcibly switching a
* controller for a new one will lead to undefined behavior.
*/
public void setPairingController(BluetoothPairingController pairingController) {
if (mPairingController != null) {
throw new IllegalStateException("The controller can only be set once. "
+ "Forcibly replacing it will lead to undefined behavior");
}
mPairingController = pairingController;
}
@@ -146,7 +159,7 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
mBuilder.setPositiveButton(getString(android.R.string.ok), this);
mBuilder.setNegativeButton(getString(android.R.string.cancel), this);
AlertDialog dialog = mBuilder.create();
dialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false);
dialog.setOnShowListener(d -> mDialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false));
return dialog;
}
@@ -171,6 +184,7 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
mPairingView = pairingView;
pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
pairingView.addTextChangedListener(this);
alphanumericPin.setOnCheckedChangeListener((buttonView, isChecked) -> {
// change input type for soft keyboard to numeric or alphanumeric
@@ -181,15 +195,21 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
}
});
int messageId = mPairingController.getDeviceVariantMessageID();
int messageIdHint = mPairingController.getDeviceVariantMessageHint();
int messageId = mPairingController.getDeviceVariantMessageId();
int messageIdHint = mPairingController.getDeviceVariantMessageHintId();
int maxLength = mPairingController.getDeviceMaxPasskeyLength();
alphanumericPin.setVisibility(mPairingController.pairingCodeIsAlphanumeric()
? View.VISIBLE : View.GONE);
messageViewCaptionHint.setText(messageIdHint);
messageView2.setText(messageId);
pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
if (messageId != BluetoothPairingController.INVALID_DIALOG_TYPE) {
messageView2.setText(messageId);
} else {
messageView2.setVisibility(View.GONE);
}
if (messageIdHint != BluetoothPairingController.INVALID_DIALOG_TYPE) {
messageViewCaptionHint.setText(messageIdHint);
} else {
messageViewCaptionHint.setVisibility(View.GONE);
}
pairingView.setFilters(new InputFilter[]{
new LengthFilter(maxLength)});
@@ -203,10 +223,8 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
mPairingController.getDeviceName()));
mBuilder.setView(createView());
mBuilder.setPositiveButton(getString(R.string.bluetooth_pairing_accept),
this);
mBuilder.setNegativeButton(getString(R.string.bluetooth_pairing_decline),
this);
mBuilder.setPositiveButton(getString(R.string.bluetooth_pairing_accept), this);
mBuilder.setNegativeButton(getString(R.string.bluetooth_pairing_decline), this);
AlertDialog dialog = mBuilder.create();
return dialog;
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2016 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 com.android.settings;
import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.Fs;
import org.robolectric.res.ResourcePath;
import java.util.List;
/**
* Custom test runner for the testing of BluetoothPairingDialogs. This is needed because the
* default behavior for robolectric is just to grab the resource directory in the target package.
* We want to override this to add several spanning different projects.
*/
public class SettingsRobolectricTestRunner extends RobolectricTestRunner {
/**
* We don't actually want to change this behavior, so we just call super.
*/
public SettingsRobolectricTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
/**
* We are going to create our own custom manifest so that we can add multiple resource
* paths to it. This lets us access resources in both Settings and SettingsLib in our tests.
*/
@Override
protected AndroidManifest getAppManifest(Config config) {
// Using the manifest file's relative path, we can figure out the application directory.
final String appRoot = "packages/apps/Settings";
final String manifestPath = appRoot + "/AndroidManifest.xml";
final String resDir = appRoot + "/res";
final String assetsDir = appRoot + "/assets";
// By adding any resources from libraries we need to the AndroidManifest, we can access
// them from within the parallel universe's resource loader.
final AndroidManifest manifest = new AndroidManifest(Fs.fileFromPath(manifestPath),
Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)) {
@Override
public List<ResourcePath> getIncludedResourcePaths() {
List<ResourcePath> paths = super.getIncludedResourcePaths();
paths.add(new ResourcePath(
getPackageName(),
Fs.fileFromPath("./packages/apps/Settings/res"),
null));
paths.add(new ResourcePath(
getPackageName(),
Fs.fileFromPath("./frameworks/base/packages/SettingsLib/res"),
null));
return paths;
}
};
// Set the package name to the renamed one
manifest.setPackageName("com.android.settings");
return manifest;
}
}

View File

@@ -0,0 +1,340 @@
/*
* Copyright (C) 2016 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 com.android.settings.bluetooth;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AlertDialog;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.R;
import com.android.settings.TestConfig;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.robolectric.util.FragmentTestUtil;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class BluetoothPairingDialogTest {
private static final String FILLER = "text that goes in a view";
private static final String FAKE_DEVICE_NAME = "Fake Bluetooth Device";
@Mock
private BluetoothPairingController controller;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void dialogUpdatesControllerWithUserInput() {
// set the correct dialog type
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
// we don't care about these for this test
when(controller.getDeviceVariantMessageId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
when(controller.getDeviceVariantMessageHintId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
// build fragment
BluetoothPairingDialogFragment frag = makeFragment();
// test that controller is updated on text change
frag.afterTextChanged(new SpannableStringBuilder(FILLER));
verify(controller, times(1)).updateUserInput(any());
}
@Test
public void dialogEnablesSubmitButtonOnValidationFromController() {
// set the correct dialog type
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
// we don't care about these for this test
when(controller.getDeviceVariantMessageId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
when(controller.getDeviceVariantMessageHintId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
// force the controller to say that any passkey is valid
when(controller.isPasskeyValid(any())).thenReturn(true);
// build fragment
BluetoothPairingDialogFragment frag = makeFragment();
// test that the positive button is enabled when passkey is valid
frag.afterTextChanged(new SpannableStringBuilder(FILLER));
View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
assertThat(button).isNotNull();
assertThat(button.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void dialogDoesNotAskForPairCodeOnConsentVariant() {
// set the dialog variant to confirmation/consent
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// check that the input field used by the entry dialog fragment does not exist
View view = frag.getmDialog().findViewById(R.id.text);
assertThat(view).isNull();
}
@Test
public void dialogAsksForPairCodeOnUserEntryVariant() {
// set the dialog variant to user entry
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
// we don't care about these for this test
when(controller.getDeviceVariantMessageId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
when(controller.getDeviceVariantMessageHintId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// check that the pin/passkey input field is visible to the user
View view = frag.getmDialog().findViewById(R.id.text);
assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void dialogDisplaysPairCodeOnDisplayPasskeyVariant() {
// set the dialog variant to display passkey
when(controller.getDialogType())
.thenReturn(BluetoothPairingController.DISPLAY_PASSKEY_DIALOG);
// ensure that the controller returns good values to indicate a passkey needs to be shown
when(controller.isDisplayPairingKeyVariant()).thenReturn(true);
when(controller.hasPairingContent()).thenReturn(true);
when(controller.getPairingContent()).thenReturn(FILLER);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// get the relevant views
View messagePairing = frag.getmDialog().findViewById(R.id.pairing_code_message);
TextView pairingViewContent =
(TextView) frag.getmDialog().findViewById(R.id.pairing_subhead);
View pairingViewCaption = frag.getmDialog().findViewById(R.id.pairing_caption);
// check that the relevant views are visible and that the passkey is shown
assertThat(messagePairing.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(pairingViewCaption.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(pairingViewContent.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(TextUtils.equals(FILLER, pairingViewContent.getText())).isTrue();
}
@Test(expected = IllegalStateException.class)
public void dialogThrowsExceptionIfNoControllerSet() {
// instantiate a fragment
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
// this should throw an error
FragmentTestUtil.startFragment(frag);
fail("Starting the fragment with no controller set should have thrown an exception.");
}
@Test
public void dialogCallsHookOnPositiveButtonPress() {
// set the dialog variant to confirmation/consent
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
// we don't care what this does, just that it is called
doNothing().when(controller).onDialogPositiveClick(any());
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// click the button and verify that the controller hook was called
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
verify(controller, times(1)).onDialogPositiveClick(any());
}
@Test
public void dialogCallsHookOnNegativeButtonPress() {
// set the dialog variant to confirmation/consent
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
// we don't care what this does, just that it is called
doNothing().when(controller).onDialogNegativeClick(any());
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// click the button and verify that the controller hook was called
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
verify(controller, times(1)).onDialogNegativeClick(any());
}
@Test(expected = IllegalStateException.class)
public void dialogDoesNotAllowSwappingController() {
// instantiate a fragment
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
frag.setPairingController(controller);
// this should throw an error
frag.setPairingController(controller);
fail("Setting the controller multiple times should throw an exception.");
}
@Test
public void dialogPositiveButtonDisabledWhenUserInputInvalid() {
// set the correct dialog type
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
// we don't care about these for this test
when(controller.getDeviceVariantMessageId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
when(controller.getDeviceVariantMessageHintId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
// force the controller to say that any passkey is valid
when(controller.isPasskeyValid(any())).thenReturn(false);
// build fragment
BluetoothPairingDialogFragment frag = makeFragment();
// test that the positive button is enabled when passkey is valid
frag.afterTextChanged(new SpannableStringBuilder(FILLER));
View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
assertThat(button).isNotNull();
assertThat(button.isEnabled()).isFalse();
}
@Test
public void dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady() {
// set the dialog variant to confirmation/consent
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
// set a fake device name and pretend the profile has not been set up for it
when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
when(controller.isProfileReady()).thenReturn(false);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// verify that the checkbox is visible and that the device name is correct
CheckBox sharingCheckbox = (CheckBox) frag.getmDialog()
.findViewById(R.id.phonebook_sharing_message_confirm_pin);
assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(sharingCheckbox.getText().toString().contains(FAKE_DEVICE_NAME)).isTrue();
}
@Test
public void dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady() {
// set the dialog variant to confirmation/consent
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
// set a fake device name and pretend the profile has been set up for it
when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
when(controller.isProfileReady()).thenReturn(true);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// verify that the checkbox is gone
CheckBox sharingCheckbox = (CheckBox) frag.getmDialog()
.findViewById(R.id.phonebook_sharing_message_confirm_pin);
assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.GONE);
}
@Test
public void dialogShowsMessageOnPinEntryView() {
// set the correct dialog type
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
// Set the message id to something specific to verify later
when(controller.getDeviceVariantMessageId()).thenReturn(R.string.cancel);
when(controller.getDeviceVariantMessageHintId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// verify message is what we expect it to be and is visible
TextView message = (TextView) frag.getmDialog().findViewById(R.id.message_below_pin);
assertThat(message.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(TextUtils.equals(frag.getString(R.string.cancel), message.getText())).isTrue();
}
@Test
public void dialogShowsMessageHintOnPinEntryView() {
// set the correct dialog type
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
// Set the message id hint to something specific to verify later
when(controller.getDeviceVariantMessageHintId()).thenReturn(R.string.cancel);
when(controller.getDeviceVariantMessageId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// verify message is what we expect it to be and is visible
TextView hint = (TextView) frag.getmDialog().findViewById(R.id.pin_values_hint);
assertThat(hint.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(TextUtils.equals(frag.getString(R.string.cancel), hint.getText())).isTrue();
}
@Test
public void dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView() {
// set the correct dialog type
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
// Set the id's to what is returned when it is not provided
when(controller.getDeviceVariantMessageHintId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
when(controller.getDeviceVariantMessageId())
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
// build the fragment
BluetoothPairingDialogFragment frag = makeFragment();
// verify message is what we expect it to be and is visible
TextView hint = (TextView) frag.getmDialog().findViewById(R.id.pin_values_hint);
assertThat(hint.getVisibility()).isEqualTo(View.GONE);
TextView message = (TextView) frag.getmDialog().findViewById(R.id.message_below_pin);
assertThat(message.getVisibility()).isEqualTo(View.GONE);
}
private BluetoothPairingDialogFragment makeFragment() {
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
frag.setPairingController(controller);
FragmentTestUtil.startFragment(frag);
assertThat(frag.getmDialog()).isNotNull();
return frag;
}
}