首页 > 代码库 > Robotium源码解读-native/webview控件的获取和操作
Robotium源码解读-native/webview控件的获取和操作
之前基本上没接触过移动端的UITest测试,之前因为一些需求临时赶鸭子上架采用了UIAutomator,但是后来发现webview没办法识别,在预研过程中,发现Robotium跟Appium这两个神器。由于Robotium提供了webview的解析方式,遂决定研究一下。
一.环境准备以及初始化
用来说明的用例采用的是Robotium官网的一个tutorial用例-Notepad
@RunWith(AndroidJUnit4.class) public class NotePadTest { private static final String NOTE_1 = "Note 1"; private static final String NOTE_2 = "Note 2"; @Rule public ActivityTestRule<NotesList> activityTestRule = new ActivityTestRule<>(NotesList.class); private Solo solo; @Before public void setUp() throws Exception { //setUp() is run before a test case is started. //This is where the solo object is created. solo = new Solo(InstrumentationRegistry.getInstrumentation(), activityTestRule.getActivity()); } @After public void tearDown() throws Exception { //tearDown() is run after a test case has finished. //finishOpenedActivities() will finish all the activities that have been opened during the test execution. solo.finishOpenedActivities(); } @Test public void testAddNote() throws Exception { //Unlock the lock screen solo.unlockScreen(); //Click on action menu item add solo.clickOnView(solo.getView(R.id.menu_add)); //Assert that NoteEditor activity is opened solo.assertCurrentActivity("Expected NoteEditor Activity", NoteEditor.class); //In text field 0, enter Note 1 solo.enterText(0, NOTE_1); //Click on action menu item Save solo.clickOnView(solo.getView(R.id.menu_save)); //Click on action menu item Add solo.clickOnView(solo.getView(R.id.menu_add)); //In text field 0, type Note 2 solo.typeText(0, NOTE_2); //Click on action menu item Save solo.clickOnView(solo.getView(R.id.menu_save)); //Takes a screenshot and saves it in "/sdcard/Robotium-Screenshots/". solo.takeScreenshot(); //Search for Note 1 and Note 2 boolean notesFound = solo.searchText(NOTE_1) && solo.searchText(NOTE_2); //To clean up after the test case deleteNotes(); //Assert that Note 1 & Note 2 are found assertTrue("Note 1 and/or Note 2 are not found", notesFound); } @Test public void testEditNoteTitle() throws Exception { //Click on add action menu item solo.clickOnView(solo.getView(com.example.android.notepad.R.id.menu_add)); //In text field 0, enter Note 1 solo.enterText(0, NOTE_1); //Press hard key back button solo.goBack(); solo.clickOnText(NOTE_1); //Click on menu item "Edit title" solo.clickOnMenuItem("Edit title"); //Clear the edit text field solo.clearEditText(0); //In the text field enter Note 2 solo.enterText(0, NOTE_2); //Click on button "OK" solo.clickOnButton("OK"); //Click on action menu item Save solo.clickOnView(solo.getView(com.example.android.notepad.R.id.menu_save)); //Long click Note 2 solo.clickLongOnText(NOTE_2); //Click on Delete solo.clickOnText("Delete"); //Assert that Note 2 is deleted assertFalse("Note 2 is found", solo.searchText(NOTE_2)); } private void deleteNotes() { //Click on first item in List solo.clickInList(1); //Click on delete action menu item solo.clickOnView(solo.getView(com.example.android.notepad.R.id.menu_delete)); //Long click first item in List solo.clickLongInList(1); //Click delete solo.clickOnText(solo.getString(R.string.menu_delete)); } }
在进行初始化时,Solo对象依赖Instrumentation对象以及被测应用的Activity对象,在这里是NotesList,然后所有的UI操作都依赖这个Solo对象。
二.Native控件解析与操作
1.Native控件解析
看一个标准的操作:solo.clickOnView(solo.getView(R.id.menu_save));
solo点击id为menu_save的控件,其中clickOnView传入参数肯定为menu_save的view对象,那这个是怎么获取的呢?
由于调用比较深,因此直接展示关键方法
public View waitForView(int id, int index, int timeout, boolean scroll) { HashSet uniqueViewsMatchingId = new HashSet(); long endTime = SystemClock.uptimeMillis() + (long)timeout; while(SystemClock.uptimeMillis() <= endTime) { this.sleeper.sleep(); Iterator i$ = this.viewFetcher.getAllViews(false).iterator(); while(i$.hasNext()) { View view = (View)i$.next(); Integer idOfView = Integer.valueOf(view.getId()); if(idOfView.equals(Integer.valueOf(id))) { uniqueViewsMatchingId.add(view); if(uniqueViewsMatchingId.size() > index) { return view; } } } if(scroll) { this.scroller.scrollDown(); } } return null; }
这个方法是先去获取所有的View: this.viewFetcher.getAllViews(false),然后通过匹配id来获取正确的View。
那Robotium是怎么获取到所有的View呢?这就要看看viewFetcher里的实现了。
public ArrayList<View> getAllViews(boolean onlySufficientlyVisible) { View[] views = this.getWindowDecorViews(); ArrayList allViews = new ArrayList(); View[] nonDecorViews = this.getNonDecorViews(views); View view = null; if(nonDecorViews != null) { for(int ignored = 0; ignored < nonDecorViews.length; ++ignored) { view = nonDecorViews[ignored]; try { this.addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible); } catch (Exception var9) { ; } if(view != null) { allViews.add(view); } } } if(views != null && views.length > 0) { view = this.getRecentDecorView(views); try { this.addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible); } catch (Exception var8) { ; } if(view != null) { allViews.add(view); } } return allViews; }
需要说明的是,DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面。
从上面的代码可以看到,allViews包括WindowDecorViews,nonDecorViews,RecentDecorView。所以,我对这三个封装比较感兴趣,他们是怎么拿到WindowDecorViews,nonDecorViews,RecentDecorView的呢?
继续看代码,可以看到以下方法(看注释)
// 获取 DecorViews public View[] getWindowDecorViews() { try { Field viewsField = windowManager.getDeclaredField("mViews"); Field instanceField = windowManager.getDeclaredField(this.windowManagerString); viewsField.setAccessible(true); instanceField.setAccessible(true); Object e = instanceField.get((Object)null); View[] result; if(VERSION.SDK_INT >= 19) { result = (View[])((ArrayList)viewsField.get(e)).toArray(new View[0]); } else { result = (View[])((View[])viewsField.get(e)); } return result; } catch (Exception var5) { var5.printStackTrace(); return null; } } // 获取NonDecorViews private final View[] getNonDecorViews(View[] views) { View[] decorViews = null; if(views != null) { decorViews = new View[views.length]; int i = 0; for(int j = 0; j < views.length; ++j) { View view = views[j]; if(!this.isDecorView(view)) { decorViews[i] = view; ++i; } } } return decorViews; } // 获取RecentDecorView public final View getRecentDecorView(View[] views) { if(views == null) { return null; } else { View[] decorViews = new View[views.length]; int i = 0; for(int j = 0; j < views.length; ++j) { View view = views[j]; if(this.isDecorView(view)) { decorViews[i] = view; ++i; } } return this.getRecentContainer(decorViews); } }
其中DecorViews就不用多说了,通过反射拿到一个里面的元素都是View的List,而NonDecorViews则是通过便利DectorViews进行过滤,nameOfClass 不满足要求的,则为NonDecorViews
String nameOfClass = view.getClass().getName(); return nameOfClass.equals("com.android.internal.policy.impl.PhoneWindow$DecorView") || nameOfClass.equals("com.android.internal.policy.impl.MultiPhoneWindow$MultiPhoneDecorView") || nameOfClass.equals("com.android.internal.policy.PhoneWindow$DecorView");
而recentlyView则通过以下条件进行判断,满足则为recentlyView
view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime
2.Native控件解析
依旧说的是这个操作:solo.clickOnView(solo.getView(R.id.menu_save));接下来要看的是clickOnView的封装了。
这部分实现相对简单很多了,获取控件坐标的中央X,Y值后,利用instrumentation的sendPointerSync来完成点击/长按操作
public void clickOnScreen(float x, float y, View view) { boolean successfull = false; int retry = 0; SecurityException ex = null; while(!successfull && retry < 20) { long downTime = SystemClock.uptimeMillis(); long eventTime = SystemClock.uptimeMillis(); MotionEvent event = MotionEvent.obtain(downTime, eventTime, 0, x, y, 0); MotionEvent event2 = MotionEvent.obtain(downTime, eventTime, 1, x, y, 0); try { this.inst.sendPointerSync(event); this.inst.sendPointerSync(event2); successfull = true; } catch (SecurityException var16) { ex = var16; this.dialogUtils.hideSoftKeyboard((EditText)null, false, true); this.sleeper.sleep(300); ++retry; View identicalView = this.viewFetcher.getIdenticalView(view); if(identicalView != null) { float[] xyToClick = this.getClickCoordinates(identicalView); x = xyToClick[0]; y = xyToClick[1]; } } } if(!successfull) { Assert.fail("Click at (" + x + ", " + y + ") can not be completed! (" + (ex != null?ex.getClass().getName() + ": " + ex.getMessage():"null") + ")"); } }
3.总结:
从源码中可以看出,其实native控件操作的思想是这样的。
通过android.view.windowManager获取到所有的view,然后经过过滤得到自己需要的view,最后通过计算view的 Coordinates得到中央坐标,最后依赖instrument来完成操作。
三.Webview的解析与操作
Robotium源码解读-native/webview控件的获取和操作