Makes it possible to robo-test Settings app fragments.
This adds bunch of shadow/placeholder classes and logic to handle references to Android internal resources or newly added classes/methods that Robolectric hasn't yet picked up. Developers can follow ManageApplicationsTest example to use the shadow classes and the utility method to start ther fragment in their robolectric tests. Bug: 33431346 Test: This is a test improvement CL. RunSettingsRoboTests still passes. Change-Id: I943ab871631cb8c368d9f9db481c00558c5c4d1f
This commit is contained in:
@@ -87,4 +87,5 @@ com.android.settings.applications.AppStorageSettings
|
||||
com.android.settings.notification.NotificationAccessSettings
|
||||
com.android.settings.notification.ZenModeSettings
|
||||
com.android.settings.accessibility.ToggleDaltonizerPreferenceFragment
|
||||
com.android.settings.applications.ConvertToFbe
|
||||
com.android.settings.applications.ConvertToFbe
|
||||
com.android.settings.localepicker.LocaleListEditor
|
19
tests/robotests/src/android/print/PrintServicesLoader.java
Normal file
19
tests/robotests/src/android/print/PrintServicesLoader.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package android.print;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.content.Loader;
|
||||
import android.printservice.PrintServiceInfo;
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A placeholder class to prevent ClassNotFound exceptions caused by lack of visibility.
|
||||
*/
|
||||
public class PrintServicesLoader extends Loader<List<PrintServiceInfo>> {
|
||||
public PrintServicesLoader(@NonNull PrintManager printManager, @NonNull Context context,
|
||||
int selectionFlags) {
|
||||
super(Preconditions.checkNotNull(context));
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.android.internal.app;
|
||||
|
||||
/**
|
||||
* A placeholder class to prevent ClassNotFound exceptions caused by lack of visibility.
|
||||
*/
|
||||
public class LocalePickerWithRegion {
|
||||
|
||||
public interface LocaleSelectedListener {
|
||||
void onLocaleSelected(LocaleStore.LocaleInfo locale);
|
||||
}
|
||||
}
|
@@ -15,15 +15,23 @@
|
||||
*/
|
||||
package com.android.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
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 org.robolectric.util.ActivityController;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT;
|
||||
import static org.robolectric.Robolectric.getShadowsAdapter;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -77,4 +85,15 @@ public class SettingsRobolectricTestRunner extends RobolectricTestRunner {
|
||||
manifest.setPackageName("com.android.settings");
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
|
||||
// A simple utility class to start a Settings fragment with an intent. The code here is almost
|
||||
// the same as FragmentTestUtil.startFragment except that it starts an activity with an intent.
|
||||
public static void startSettingsFragment(
|
||||
Fragment fragment, Class<? extends SettingsActivity> activityClass) {
|
||||
Intent intent = new Intent().putExtra(EXTRA_SHOW_FRAGMENT, fragment.getClass().getName());
|
||||
SettingsActivity activity = ActivityController.of(
|
||||
getShadowsAdapter(), ReflectionHelpers.callConstructor(activityClass), intent)
|
||||
.setup().get();
|
||||
activity.getFragmentManager().beginTransaction().add(fragment, null).commit();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,61 @@
|
||||
package com.android.settings.applications;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.os.UserManager;
|
||||
import com.android.settings.Settings;
|
||||
import com.android.settings.SettingsRobolectricTestRunner;
|
||||
import com.android.settings.TestConfig;
|
||||
import com.android.settings.testutils.shadow.ShadowDynamicIndexableContentMonitor;
|
||||
import com.android.settings.testutils.shadow.SettingsShadowResources;
|
||||
import com.android.settings.testutils.shadow.SettingsShadowResources.SettingsShadowTheme;
|
||||
import com.android.settingslib.applications.ApplicationsState;
|
||||
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.ReflectionHelpers;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link ManageApplications}.
|
||||
*/
|
||||
@RunWith(SettingsRobolectricTestRunner.class)
|
||||
// TODO: Consider making the shadow class set global using a robolectric.properties file.
|
||||
@Config(manifest = TestConfig.MANIFEST_PATH,
|
||||
sdk = TestConfig.SDK_VERSION,
|
||||
shadows = {
|
||||
SettingsShadowResources.class,
|
||||
SettingsShadowTheme.class,
|
||||
ShadowDynamicIndexableContentMonitor.class
|
||||
})
|
||||
public class ManageApplicationsTest {
|
||||
|
||||
@Mock private ApplicationsState mState;
|
||||
@Mock private ApplicationsState.Session mSession;
|
||||
@Mock private UserManager mUserManager;
|
||||
|
||||
private Looper mBgLooper;
|
||||
|
||||
private ManageApplications mFragment;
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
ReflectionHelpers.setStaticField(ApplicationsState.class, "sInstance", mState);
|
||||
when(mState.newSession(any())).thenReturn(mSession);
|
||||
mBgLooper = Looper.myLooper();
|
||||
when(mState.getBackgroundLooper()).thenReturn(mBgLooper);
|
||||
|
||||
mFragment = new ManageApplications();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void launchFragment() {
|
||||
SettingsRobolectricTestRunner.startSettingsFragment(
|
||||
mFragment, Settings.ManageApplicationsActivity.class);
|
||||
}
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
package com.android.settings.testutils.shadow;
|
||||
|
||||
import android.annotation.DimenRes;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.Resources.NotFoundException;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
import org.robolectric.annotation.RealObject;
|
||||
import org.robolectric.res.StyleData;
|
||||
import org.robolectric.res.StyleResolver;
|
||||
import org.robolectric.res.builder.XmlResourceParserImpl;
|
||||
import org.robolectric.shadows.ShadowAssetManager;
|
||||
import org.robolectric.shadows.ShadowResources;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static android.util.TypedValue.TYPE_REFERENCE;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
import static org.robolectric.internal.Shadow.directlyOn;
|
||||
|
||||
/**
|
||||
* Shadow Resources and Theme classes to handle resource references that Robolectric shadows cannot
|
||||
* handle because they are too new or private.
|
||||
*/
|
||||
@Implements(Resources.class)
|
||||
public class SettingsShadowResources extends ShadowResources {
|
||||
|
||||
@RealObject Resources realResources;
|
||||
|
||||
@Implementation
|
||||
public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException {
|
||||
// Handle requests for private dimension resources,
|
||||
// TODO: Consider making a set of private dimension resource ids if this happens repeatedly.
|
||||
if (id == com.android.internal.R.dimen.preference_fragment_padding_bottom) {
|
||||
return 0;
|
||||
}
|
||||
return directlyOn(realResources, Resources.class).getDimensionPixelSize(id);
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public Drawable loadDrawable(TypedValue value, int id, Theme theme)
|
||||
throws NotFoundException {
|
||||
// The drawable item in switchbar_background.xml refers to a very recent color attribute
|
||||
// that Robolectric isn't yet aware of.
|
||||
// TODO: Remove this once Robolectric is updated.
|
||||
if (id == com.android.settings.R.drawable.switchbar_background) {
|
||||
return new ColorDrawable();
|
||||
}
|
||||
return super.loadDrawable(value, id, theme);
|
||||
}
|
||||
|
||||
@Implements(Theme.class)
|
||||
public static class SettingsShadowTheme extends ShadowTheme {
|
||||
|
||||
@RealObject
|
||||
Theme realTheme;
|
||||
|
||||
@Implementation
|
||||
public TypedArray obtainStyledAttributes(
|
||||
AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
|
||||
// Replace all private string references with a placeholder.
|
||||
if (set != null) {
|
||||
for (int i = 0; i < set.getAttributeCount(); ++i) {
|
||||
if (set.getAttributeValue(i).startsWith("@*android:string")) {
|
||||
Node node = ReflectionHelpers.callInstanceMethod(
|
||||
XmlResourceParserImpl.class, set, "getAttributeAt",
|
||||
ReflectionHelpers.ClassParameter.from(int.class, i));
|
||||
node.setNodeValue("PLACEHOLDER");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track down all styles and remove all inheritance from private styles.
|
||||
ShadowAssetManager assetManager = shadowOf(RuntimeEnvironment.application.getAssets());
|
||||
// The Object's below are actually ShadowAssetManager.OverlayedStyle. We can't use it
|
||||
// here because it's package private.
|
||||
Map<Long, List<Object>> appliedStylesList =
|
||||
ReflectionHelpers.getField(assetManager, "appliedStyles");
|
||||
for (Long idx : appliedStylesList.keySet()) {
|
||||
List<Object> appliedStyles = appliedStylesList.get(idx);
|
||||
int i = 1;
|
||||
for (Object appliedStyle : appliedStyles) {
|
||||
StyleResolver styleResolver = ReflectionHelpers.getField(appliedStyle, "style");
|
||||
List<StyleData> styleDatas =
|
||||
ReflectionHelpers.getField(styleResolver, "styles");
|
||||
for (StyleData styleData : styleDatas) {
|
||||
if (styleData.getParent() != null &&
|
||||
styleData.getParent().startsWith("@*android:style")) {
|
||||
ReflectionHelpers.setField(StyleData.class, styleData, "parent", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return super.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) {
|
||||
// The real Resources instance in Robolectric tests somehow fails to find the
|
||||
// preferenceTheme attribute in the layout. Let's do it ourselves.
|
||||
if (getResources().getResourceName(resid)
|
||||
.equals("com.android.settings:attr/preferenceTheme")) {
|
||||
int preferenceThemeResId =
|
||||
getResources().getIdentifier(
|
||||
"PreferenceTheme", "style", "com.android.settings");
|
||||
outValue.type = TYPE_REFERENCE;
|
||||
outValue.data = preferenceThemeResId;
|
||||
outValue.resourceId = preferenceThemeResId;
|
||||
return true;
|
||||
}
|
||||
return directlyOn(realTheme, Theme.class)
|
||||
.resolveAttribute(resid, outValue, resolveRefs);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.android.settings.testutils.shadow;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.UserManager;
|
||||
import com.android.settings.search.DynamicIndexableContentMonitor;
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
import org.robolectric.annotation.RealObject;
|
||||
|
||||
/**
|
||||
* A shadow class of {@link DynamicIndexableContentMonitor}. The real implementation of
|
||||
* {@link DynamicIndexableContentMonitor#register} calls {@link UserManager#isUserUnlocked()}, which
|
||||
* Robolectric has not yet been updated to support, so throws a NoSuchMethodError exception.
|
||||
*/
|
||||
// TODO: Delete this once Robolectric is updated to the latest SDK.
|
||||
@Implements(DynamicIndexableContentMonitor.class)
|
||||
public class ShadowDynamicIndexableContentMonitor {
|
||||
|
||||
@Implementation
|
||||
public void register(Activity activity, int loaderId) {
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user