package com.mixpanel.android.viewcrawler; import android.annotation.TargetApi; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.test.AndroidTestCase; import android.util.JsonWriter; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.mixpanel.android.mpmetrics.MPConfig; import com.mixpanel.android.mpmetrics.ResourceIds; import com.mixpanel.android.mpmetrics.TestUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class ViewSnapshotTest extends AndroidTestCase { public void setUp() throws NoSuchMethodException { mRootView = new TestView(this.getContext()); final List<PropertyDescription> props = new ArrayList<PropertyDescription>(); final Caller textGetter = new Caller(TextView.class, "getText", new Object[0], CharSequence.class); final PropertyDescription text = new PropertyDescription("text", TextView.class, textGetter, "setText"); props.add(text); final Caller customPropGetter = new Caller(TestView.CustomPropButton.class, "getCustomProperty", new Object[0], CharSequence.class); final PropertyDescription custom = new PropertyDescription( "custom", TestView.CustomPropButton.class, customPropGetter, "setCustomProperty" ); props.add(custom); final Caller drawableGetter = new Caller(ImageView.class, "getDrawable", new Object[0], Drawable.class); final PropertyDescription drawable = new PropertyDescription( "drawable", ImageView.class, drawableGetter, "setImageDrawable" ); props.add(drawable); final Map<String, Integer> idNamesToIds = new HashMap<String, Integer>(); idNamesToIds.put("ROOT_ID", TestView.ROOT_ID); idNamesToIds.put("TEXT_VIEW_ID", TestView.TEXT_VIEW_ID); idNamesToIds.put("CRAZYSAUCE ID", 1234567); // NO BUTTON_ID in the table final ResourceIds resourceIds = new TestUtils.TestResourceIds(idNamesToIds); mSnapshot = new ViewSnapshot(getContext(), props, resourceIds); int width = View.MeasureSpec.makeMeasureSpec(768, View.MeasureSpec.EXACTLY); int height = View.MeasureSpec.makeMeasureSpec(1280, View.MeasureSpec.EXACTLY); mRootView.measure(width, height); mRootView.layout(0, 0, 768, 1280); } public void testBadMethods() { try { final Caller crazyGetter = new Caller(View.class, "CRAZY GETTER", new Object[0], Void.TYPE); fail("Exception was not thrown when constructing a bad caller"); } catch (NoSuchMethodException e) { // OK! } try { final Caller badTypesGetter = new Caller(TextView.class, "getText", new Object[0], Integer.class); fail("Exception was not thrown when constructing a caller with bad types"); } catch (NoSuchMethodException e) { // OK! } } public void testDrawableSnapshot() throws IOException, JSONException { final Drawable realDrawable = mRootView.mImageView.getDrawable(); final SparseArray<JSONObject> descsByHashcode = snapshotsByHashcode(); final JSONObject imageDesc = descsByHashcode.get(mRootView.mImageView.hashCode()); final JSONObject drawableDesc = imageDesc.getJSONObject("drawable"); final JSONArray classes = drawableDesc.getJSONArray("classes"); assertEquals("android.graphics.drawable.BitmapDrawable", classes.get(0)); assertEquals("android.graphics.drawable.Drawable", classes.get(1)); final Rect realBounds = realDrawable.getBounds(); final JSONObject dimensions = drawableDesc.getJSONObject("dimensions"); final int top = dimensions.getInt("top"); final int bottom = dimensions.getInt("bottom"); final int left = dimensions.getInt("left"); final int right = dimensions.getInt("right"); assertEquals(realBounds.top, top); assertEquals(realBounds.bottom, bottom); assertEquals(realBounds.left, left); assertEquals(realBounds.right, right); } public void testSnapshotComplete() throws IOException, JSONException { final JSONArray viewsJson = requestSnapshot(); assertEquals(mRootView.mAllViews.size(), viewsJson.length()); } public void testCommonProperties() throws IOException, JSONException { final SparseArray<JSONObject> descsByHashcode = snapshotsByHashcode(); final Map<Integer, View> viewsByHashCode = mRootView.mViewsByHashcode; final int size = descsByHashcode.size(); for (int i = 0; i < size; i++) { final int hashCode = descsByHashcode.keyAt(i); final JSONObject viewDesc = descsByHashcode.valueAt(i); final View found = viewsByHashCode.get(hashCode); assertEquals(found.getId(), viewDesc.getInt("id")); assertEquals(found.getHeight(), viewDesc.getInt("height")); assertEquals(found.getWidth(), viewDesc.getInt("width")); final CharSequence foundContentDescription = found.getContentDescription(); if (null == foundContentDescription) { assertTrue(viewDesc.isNull("contentDescription")); } else { assertEquals(foundContentDescription.toString(), viewDesc.getString("contentDescription")); } } } public void testChildrenAccountedFor() throws IOException, JSONException { final SparseArray<JSONObject> descsByHashcode = snapshotsByHashcode(); final Map<Integer, View> viewsByHashCode = mRootView.mViewsByHashcode; final int size = descsByHashcode.size(); for (int i = 0; i < size; i++) { final int hashCode = descsByHashcode.keyAt(i); final JSONObject viewDesc = descsByHashcode.valueAt(i); final View found = viewsByHashCode.get(hashCode); final JSONArray children = viewDesc.getJSONArray("subviews"); if (found instanceof ViewGroup) { final ViewGroup group = (ViewGroup) found; final int childCount = group.getChildCount(); assertEquals(childCount, children.length()); final Set<Integer> expectHashCodes = new HashSet<Integer>(); final Set<Integer> snapshotHashCodes = new HashSet<Integer>(); for (int childIx = 0; childIx < childCount; childIx++) { expectHashCodes.add(group.getChildAt(childIx).hashCode()); snapshotHashCodes.add(children.getInt(childIx)); } assertEquals(expectHashCodes, snapshotHashCodes); } } } public void testViewSpecificProperties() throws IOException, JSONException { final SparseArray<JSONObject> descsByHashcode = snapshotsByHashcode(); final JSONObject adhoc1Desc = descsByHashcode.get(mRootView.mAdHocButton1.hashCode()); final JSONObject adhoc2Desc = descsByHashcode.get(mRootView.mAdHocButton2.hashCode()); final JSONObject adhoc3Desc = descsByHashcode.get(mRootView.mAdHocButton3.hashCode()); assertEquals(mRootView.mAdHocButton1.getText(), adhoc1Desc.getString("text")); assertEquals(mRootView.mAdHocButton2.getText(), adhoc2Desc.getString("text")); assertEquals(mRootView.mAdHocButton3.getText(), adhoc3Desc.getString("text")); assertEquals(mRootView.mAdHocButton1.getCustomProperty(), adhoc1Desc.getString("custom")); assertFalse(adhoc2Desc.has("custom")); assertFalse(adhoc3Desc.has("custom")); assertFalse(adhoc1Desc.has("crazy")); assertFalse(adhoc1Desc.has("badTypes")); assertFalse(adhoc2Desc.has("crazy")); assertFalse(adhoc2Desc.has("badTypes")); assertFalse(adhoc3Desc.has("crazy")); assertFalse(adhoc3Desc.has("badTypes")); final JSONObject rootDesc = descsByHashcode.get(mRootView.hashCode()); final JSONObject textViewDesc = descsByHashcode.get(mRootView.mTextView1.hashCode()); assertEquals(rootDesc.getString("mp_id_name"), "ROOT_ID"); assertEquals(textViewDesc.getString("mp_id_name"), "TEXT_VIEW_ID"); assertEquals(adhoc3Desc.get("mp_id_name"), JSONObject.NULL); assertEquals(adhoc3Desc.getInt("id"), TestView.BUTTON_ID); } private SparseArray<JSONObject> snapshotsByHashcode() throws IOException, JSONException { final JSONArray viewsJson = requestSnapshot(); final Map<Integer, View> viewsByHashcode = new HashMap<Integer, View>(mRootView.mViewsByHashcode); final SparseArray<JSONObject> ret = new SparseArray<JSONObject>(); for (int viewIx = 0; viewIx < viewsJson.length(); viewIx++) { final JSONObject viewDesc = viewsJson.getJSONObject(viewIx); final int descHashcode = viewDesc.getInt("hashCode"); ret.put(descHashcode, viewDesc); final View found = viewsByHashcode.remove(descHashcode); assertNotNull(found); } assertTrue(viewsByHashcode.isEmpty()); return ret; } @TargetApi(MPConfig.UI_FEATURES_MIN_API) private JSONArray requestSnapshot() throws IOException, JSONException { final ByteArrayOutputStream out = new ByteArrayOutputStream(); final OutputStreamWriter writer = new OutputStreamWriter(out); final JsonWriter j = new JsonWriter(writer); mSnapshot.snapshotViewHierarchy(j, mRootView); j.flush(); return new JSONArray(new String(out.toByteArray())); } private ViewSnapshot mSnapshot; private TestView mRootView; }