package com.mixpanel.android.viewcrawler; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.test.AndroidTestCase; import android.util.Base64; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import com.mixpanel.android.mpmetrics.ResourceIds; import com.mixpanel.android.mpmetrics.TestUtils; import com.mixpanel.android.util.ImageStore; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class EditProtocolTest extends AndroidTestCase { @Override public void setUp() throws JSONException { final Map<String, Integer> idMap = new HashMap<String, Integer>(); idMap.put("NAME PRESENT", 1001); idMap.put("ALSO PRESENT", 1002); idMap.put("relativelayout_button1", TestView.RELATIVE_LAYOUT_BUTTON1_ID); idMap.put("relativelayout_button2", TestView.RELATIVE_LAYOUT_BUTTON2_ID); mLayoutErrorListener = new TestView.MockOnLayoutErrorListener(); mResourceIds = new TestUtils.TestResourceIds(idMap); mProtocol = new EditProtocol(getContext(), mResourceIds, new ImageStore(getContext(), "EditProtocolTest") { @Override public Bitmap getImage(String url) { fail("Unexpected call to getImage"); return null; } }, mLayoutErrorListener); mSnapshotConfig = new JSONObject( "{\"config\": {\"classes\":[{\"name\":\"android.view.View\",\"properties\":[{\"name\":\"importantForAccessibility\",\"get\":{\"selector\":\"isImportantForAccessibility\",\"parameters\":[],\"result\":{\"type\":\"java.lang.Boolean\"}}}]},{\"name\":\"android.widget.TextView\",\"properties\":[{\"name\":\"text\",\"get\":{\"selector\":\"getText\",\"parameters\":[],\"result\":{\"type\":\"java.lang.CharSequence\"}},\"set\":{\"selector\":\"setText\",\"parameters\":[{\"type\":\"java.lang.CharSequence\"}]}}]},{\"name\":\"android.widget.ImageView\",\"properties\":[{\"name\":\"image\",\"set\":{\"selector\":\"setImageDrawable\",\"parameters\":[{\"type\":\"android.graphics.drawable.Drawable\"}]}}]}]}}" ); mPropertyEdit = new JSONObject( "{\"path\":[{\"view_class\":\"com.mixpanel.android.viewcrawler.TestView\",\"index\":0},{\"view_class\":\"android.widget.LinearLayout\",\"index\":0},{\"view_class\":\"android.widget.LinearLayout\",\"index\":0},{\"view_class\":\"android.widget.Button\",\"index\":1}],\"property\":{\"classname\":\"android.widget.Button\",\"name\":\"text\",\"get\":{\"selector\":\"getText\",\"parameters\":[],\"result\":{\"type\":\"java.lang.CharSequence\"}},\"set\":{\"selector\":\"setText\",\"parameters\":[{\"type\":\"java.lang.CharSequence\"}]}},\"args\":[[\"Ground Control to Major Tom\",\"java.lang.CharSequence\"]],\"change_type\": \"property\"}" ); mLayoutEditAlignParentRight = new JSONObject( String.format("{\"args\":[{\"verb\":%d,\"anchor_id_name\":\"%d\",\"view_id_name\":\"relativelayout_button1\"}],\"path\": [{\"prefix\": \"shortest\", \"index\": 0, \"id\": %d }],\"change_type\": \"layout\", \"name\": \"test1\"}", RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE, TestView.RELATIVE_LAYOUT_ID) ); mLayoutEditBelow = new JSONObject( String.format("{\"args\":[{\"verb\":%d,\"anchor_id_name\":\"relativelayout_button1\",\"view_id_name\":\"relativelayout_button2\"}],\"path\": [{\"prefix\": \"shortest\", \"index\": 0, \"id\": %d }],\"change_type\": \"layout\", \"name\": \"test2\"}", RelativeLayout.BELOW, TestView.RELATIVE_LAYOUT_ID) ); mLayoutEditRemoveBelow = new JSONObject( String.format("{\"args\":[{\"verb\":%d,\"anchor_id_name\":\"%d\",\"view_id_name\":\"relativelayout_button2\"}],\"path\": [{\"prefix\": \"shortest\", \"index\": 0, \"id\": %d }],\"change_type\": \"layout\", \"name\": \"test3\"}", RelativeLayout.BELOW, TestView.NO_ANCHOR, TestView.RELATIVE_LAYOUT_ID) ); mLayoutEditAbsentAnchor = new JSONObject( String.format("{\"args\":[{\"verb\":%d,\"anchor_id_name\":\"relativelayout_button3\",\"view_id_name\":\"relativelayout_button2\"}],\"path\": [{\"prefix\": \"shortest\", \"index\": 0, \"id\": %d }],\"change_type\": \"layout\", \"name\": \"test4\"}", RelativeLayout.BELOW, TestView.RELATIVE_LAYOUT_ID) ); mClickEvent = new JSONObject( "{\"path\":[{\"view_class\":\"com.mixpanel.android.viewcrawler.TestView\",\"index\":0},{\"view_class\":\"android.widget.LinearLayout\",\"index\":0},{\"view_class\":\"android.widget.LinearLayout\",\"index\":0},{\"view_class\":\"android.widget.Button\",\"index\":1}],\"event_type\":\"click\",\"event_name\":\"Commencing Count-Down\"}" ); mAppearsEvent = new JSONObject( "{\"path\":[{\"view_class\":\"com.mixpanel.android.viewcrawler.TestView\",\"index\":0},{\"view_class\":\"android.widget.LinearLayout\",\"index\":0},{\"view_class\":\"android.widget.LinearLayout\",\"index\":0},{\"view_class\":\"android.widget.Button\",\"index\":3}],\"event_type\":\"detected\",\"event_name\":\"Engines On!\"}" ); mTextEdit = new JSONObject( "{\"args\":[[\"Hello\",\"java.lang.CharSequence\"]],\"name\":\"c236\",\"path\":[{\"prefix\":\"shortest\",\"index\":0,\"id\":" + TestView.TEXT2_VIEW_ID + "}],\"change_type\": \"property\",\"property\":{\"name\":\"text\",\"get\":{\"selector\":\"getText\",\"parameters\":[],\"result\":{\"type\":\"java.lang.CharSequence\"}},\"set\":{\"selector\":\"setText\",\"parameters\":[{\"type\":\"java.lang.CharSequence\"}]},\"classname\":\"android.widget.TextView\"}}" ); mJustClassPath = new JSONArray("[{},{},{},{\"view_class\":\"android.widget.Button\"}]"); mJustIdPath = new JSONArray("[{},{},{},{\"id\": 2000}]"); mJustIndexPath = new JSONArray("[{},{},{},{\"index\": 2}]"); mJustTagPath = new JSONArray("[{},{},{},{\"tag\": \"this_is_a_simple_tag\"}]"); mJustIdNamePath = new JSONArray("[{},{},{},{\"mp_id_name\": \"NAME PRESENT\"}]"); mIdNameAndIdPath = new JSONArray("[{},{},{},{\"mp_id_name\": \"NAME PRESENT\", \"id\": 1001}]"); mJustFindIdPath = new JSONArray("[{},{},{},{\"prefix\": \"shortest\", \"id\": 1001}]"); mJustFindNamePath = new JSONArray("[{},{},{},{\"prefix\": \"shortest\", \"mp_id_name\": \"NAME PRESENT\"}]"); mUselessFindIdPath = new JSONArray("[{},{},{},{\"prefix\": \"shortest\", \"mp_id_name\": \"NAME PRESENT\", \"id\": 1001}]"); mIdAndNameDontMatch = new JSONArray("[{},{},{},{\"mp_id_name\": \"NO SUCH NAME\", \"id\": 90210}]"); mListener = new TestEventListener(); mRootView = new TestView(getContext()); } public void testSnapshotConfig() throws EditProtocol.BadInstructionsException { final ViewSnapshot snapshot = mProtocol.readSnapshotConfig(mSnapshotConfig); final List<PropertyDescription> properties = snapshot.getProperties(); assertEquals(properties.size(), 3); final PropertyDescription prop1 = properties.get(0); final PropertyDescription prop2 = properties.get(1); final PropertyDescription prop3 = properties.get(2); assertEquals(prop1.name, "importantForAccessibility"); assertEquals(prop2.name, "text"); assertEquals(prop3.name, "image"); assertEquals(prop1.targetClass, View.class); assertEquals(prop2.targetClass, TextView.class); assertEquals(prop3.targetClass, ImageView.class); } public void testReadPaths() throws JSONException { { final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustClassPath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.ZERO_LENGTH_PREFIX, last.prefix); assertEquals("android.widget.Button", last.viewClassName); assertEquals(-1, last.index); assertEquals(-1, last.viewId); assertEquals(null, last.tag); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustIdPath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.ZERO_LENGTH_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(-1, last.index); assertEquals(2000, last.viewId); assertEquals(null, last.tag); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustIndexPath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.ZERO_LENGTH_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(2, last.index); assertEquals(-1, last.viewId); assertEquals(null, last.tag); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustTagPath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.ZERO_LENGTH_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(-1, last.index); assertEquals(-1, last.viewId); assertEquals("this_is_a_simple_tag", last.tag); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustIdNamePath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.ZERO_LENGTH_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(-1, last.index); assertEquals(1001, last.viewId); assertEquals(null, last.tag); } { final ResourceIds emptyIds = new TestUtils.TestResourceIds(new HashMap<String, Integer>()); final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustIdNamePath, emptyIds); assertTrue(p.isEmpty()); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mIdNameAndIdPath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.ZERO_LENGTH_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(-1, last.index); assertEquals(1001, last.viewId); assertEquals(null, last.tag); } { final Map<String, Integer> nonMatchingIdMap = new HashMap<String, Integer>(); nonMatchingIdMap.put("NAME PRESENT", 1985); final ResourceIds resourceIds = new TestUtils.TestResourceIds(nonMatchingIdMap); final List<Pathfinder.PathElement> p = mProtocol.readPath(mIdNameAndIdPath, resourceIds); assertTrue(p.isEmpty()); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustFindIdPath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.SHORTEST_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(-1, last.index); assertEquals(1001, last.viewId); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mJustFindNamePath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.SHORTEST_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(-1, last.index); assertEquals(1001, last.viewId); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mUselessFindIdPath, mResourceIds); final Pathfinder.PathElement first = p.get(0); final Pathfinder.PathElement last = p.get(p.size() - 1); assertEquals(null, first.viewClassName); assertEquals(-1, first.index); assertEquals(-1, first.viewId); assertEquals(null, first.tag); assertEquals(Pathfinder.PathElement.SHORTEST_PREFIX, last.prefix); assertEquals(null, last.viewClassName); assertEquals(-1, last.index); assertEquals(1001, last.viewId); } { final List<Pathfinder.PathElement> p = mProtocol.readPath(mIdAndNameDontMatch, mResourceIds); assertTrue(p.isEmpty()); } } public void testPropertyEdit() throws EditProtocol.BadInstructionsException, EditProtocol.CantGetEditAssetsException { final EditProtocol.Edit edit = mProtocol.readEdit(mPropertyEdit); edit.visitor.visit(mRootView); assertEquals(mRootView.mAdHocButton2.getText(), "Ground Control to Major Tom"); } public void testLayoutEdit() throws EditProtocol.BadInstructionsException, EditProtocol.CantGetEditAssetsException { // add ALIGN_PARENT_RIGHT to mRelativeLayoutButton1, should success final EditProtocol.Edit edit1 = mProtocol.readEdit(mLayoutEditAlignParentRight); edit1.visitor.visit(mRootView); RelativeLayout.LayoutParams params1 = (RelativeLayout.LayoutParams)mRootView.mRelativeLayoutButton1.getLayoutParams(); int[] rules1 = params1.getRules(); assertEquals(RelativeLayout.TRUE, rules1[RelativeLayout.ALIGN_PARENT_RIGHT]); // add "BELOW mRelativeLayoutButton1" to mRelativeLayoutButton2, should success final EditProtocol.Edit edit2 = mProtocol.readEdit(mLayoutEditBelow); edit2.visitor.visit(mRootView); RelativeLayout.LayoutParams params2 = (RelativeLayout.LayoutParams)mRootView.mRelativeLayoutButton2.getLayoutParams(); int[] rules2 = params2.getRules(); assertEquals(TestView.RELATIVE_LAYOUT_BUTTON1_ID, rules2[RelativeLayout.BELOW]); // remove "BELOW mRelativeLayoutButton1" from mRelativeLayoutButton2, should success final EditProtocol.Edit edit3 = mProtocol.readEdit(mLayoutEditRemoveBelow); edit3.visitor.visit(mRootView); RelativeLayout.LayoutParams params3 = (RelativeLayout.LayoutParams)mRootView.mRelativeLayoutButton2.getLayoutParams(); int[] rule3 = params3.getRules(); assertEquals(TestView.NO_ANCHOR, rule3[RelativeLayout.BELOW]); // add "BELOW mRelativeLayoutButton3 (absent)" to mRelativeLayoutButton2, should fail final EditProtocol.Edit edit4 = mProtocol.readEdit(mLayoutEditAbsentAnchor); edit4.visitor.visit(mRootView); RelativeLayout.LayoutParams params4 = (RelativeLayout.LayoutParams)mRootView.mRelativeLayoutButton2.getLayoutParams(); int[] rules4 = params4.getRules(); assertEquals( TestView.NO_ANCHOR, rules4[RelativeLayout.BELOW]); } public void testClickEvent() throws EditProtocol.BadInstructionsException { final ViewVisitor eventListener = mProtocol.readEventBinding(mClickEvent, mListener); eventListener.visit(mRootView); mRootView.mAdHocButton2.performClick(); assertEquals(mListener.visitsRecorded.size(), 1); assertEquals(mListener.visitsRecorded.get(0), "Commencing Count-Down"); } public void testAppearsEvent() throws EditProtocol.BadInstructionsException { final ViewVisitor appearsListener = mProtocol.readEventBinding(mAppearsEvent, mListener); appearsListener.visit(mRootView); assertTrue(mListener.visitsRecorded.isEmpty()); mRootView.mButtonGroup.addView(new Button(getContext())); appearsListener.visit(mRootView); assertEquals(mListener.visitsRecorded.size(), 1); assertEquals(mListener.visitsRecorded.get(0), "Engines On!"); } public void testEdit() throws EditProtocol.BadInstructionsException, EditProtocol.CantGetEditAssetsException { final EditProtocol.Edit textEdit = mProtocol.readEdit(mTextEdit); textEdit.visitor.visit(mRootView); assertEquals("Hello", mRootView.mTextView2.getText()); textEdit.visitor.cleanup(); assertEquals("Original Text", mRootView.mTextView2.getText()); } public void testEditWithImage() throws JSONException, EditProtocol.BadInstructionsException, EditProtocol.CantGetEditAssetsException { final ResourceIds resourceIds = new TestUtils.TestResourceIds(new HashMap<String, Integer>()); final EditProtocol protocol = new EditProtocol(getContext(), resourceIds, new ImageStore(getContext(), "testEditWithImage") { @Override public Bitmap getImage(String url) { assertEquals("TEST URL", url); return IMAGE_10x10_GREEN; } }, mLayoutErrorListener); final JSONObject obj = new JSONObject( "{\"args\":[[{\"url\":\"TEST URL\", \"dimensions\":{\"left\":10,\"right\":20,\"top\":40,\"bottom\":50}},\"android.graphics.drawable.Drawable\"]],\"name\":\"test\",\"path\":[{\"prefix\":\"shortest\",\"index\":0,\"id\":" + TestView.IMAGE_VIEW_ID + "}],\"change_type\": \"property\",\"property\":{\"name\":\"image\",\"get\":{\"selector\":\"getDrawable\",\"parameters\":[],\"result\":{\"type\":\"android.graphics.drawable.Drawable\"}},\"set\":{\"selector\":\"setImageDrawable\",\"parameters\":[{\"type\":\"android.graphics.drawable.Drawable\"}]},\"classname\":\"android.widget.ImageView\"}}" ); final EditProtocol.Edit imageEdit = protocol.readEdit(obj); assertEquals(1, imageEdit.imageUrls.size()); assertEquals("TEST URL", imageEdit.imageUrls.get(0)); imageEdit.visitor.visit(mRootView); final BitmapDrawable drawable = (BitmapDrawable) mRootView.mImageView.getDrawable(); final Bitmap bmp = drawable.getBitmap(); for (int x = 0; x < bmp.getWidth(); x++) { for (int y = 0; y < bmp.getHeight(); y++) { assertEquals( IMAGE_10x10_GREEN.getPixel(x, y), bmp.getPixel(x, y) ); } } } public void testWithMissingImage() throws JSONException, EditProtocol.BadInstructionsException { final ResourceIds resourceIds = new TestUtils.TestResourceIds(new HashMap<String, Integer>()); final EditProtocol protocol = new EditProtocol(getContext(), resourceIds, new ImageStore(getContext(), "testWithMissingImage") { @Override public Bitmap getImage(String url) throws CantGetImageException { assertEquals("TEST URL", url); throw new CantGetImageException("Bang!"); } }, mLayoutErrorListener); final JSONObject obj = new JSONObject( "{\"args\":[[{\"url\":\"TEST URL\", \"dimensions\":{\"left\":10,\"right\":20,\"top\":40,\"bottom\":50}},\"android.graphics.drawable.Drawable\"]],\"name\":\"test\",\"path\":[{\"prefix\":\"shortest\",\"index\":0,\"id\":" + TestView.IMAGE_VIEW_ID + "}],\"change_type\": \"property\",\"property\":{\"name\":\"image\",\"get\":{\"selector\":\"getDrawable\",\"parameters\":[],\"result\":{\"type\":\"android.graphics.drawable.Drawable\"}},\"set\":{\"selector\":\"setImageDrawable\",\"parameters\":[{\"type\":\"android.graphics.drawable.Drawable\"}]},\"classname\":\"android.widget.ImageView\"}}" ); try { final EditProtocol.Edit imageEdit = protocol.readEdit(obj); fail("Expected a CantGetEditAssetsException to be thrown"); } catch (EditProtocol.CantGetEditAssetsException e) { ; // ok! expected this } } private static class TestEventListener implements ViewVisitor.OnEventListener { @Override public void OnEvent(View v, String eventName, boolean debounce) { visitsRecorded.add(eventName); } public List<String> visitsRecorded = new ArrayList<String>(); } private EditProtocol mProtocol; private ResourceIds mResourceIds; private JSONObject mSnapshotConfig; private JSONObject mPropertyEdit; private JSONObject mLayoutEditAlignParentRight; private JSONObject mLayoutEditBelow; private JSONObject mLayoutEditAbove; private JSONObject mLayoutEditRemoveBelow; private JSONObject mLayoutEditAbsentAnchor; private JSONObject mClickEvent; private JSONObject mAppearsEvent; private JSONObject mTextEdit; private JSONArray mJustClassPath; private JSONArray mJustIdPath; private JSONArray mJustIndexPath; private JSONArray mJustTagPath; private JSONArray mJustIdNamePath; private JSONArray mIdNameAndIdPath; private JSONArray mJustFindIdPath; private JSONArray mJustFindNamePath; private JSONArray mUselessFindIdPath; private JSONArray mIdAndNameDontMatch; private TestEventListener mListener; private TestView mRootView; private TestView.MockOnLayoutErrorListener mLayoutErrorListener; private static final byte[] IMAGE_10x10_GREEN_BYTES = Base64.decode("R0lGODlhCgAKALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//////ywAAAAACgAKAAAEClDJSau9OOvNe44AOw==".getBytes(), 0); private static final Bitmap IMAGE_10x10_GREEN = BitmapFactory.decodeByteArray(IMAGE_10x10_GREEN_BYTES, 0, IMAGE_10x10_GREEN_BYTES.length); }