|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.tests; |
|
6 |
|
7 import java.io.File; |
|
8 import java.io.IOException; |
|
9 import java.io.InputStream; |
|
10 import java.io.PrintWriter; |
|
11 import java.io.StringWriter; |
|
12 import java.util.ArrayList; |
|
13 |
|
14 import org.json.JSONArray; |
|
15 import org.json.JSONException; |
|
16 import org.json.JSONObject; |
|
17 import org.mozilla.gecko.Actions; |
|
18 import org.mozilla.gecko.Driver; |
|
19 import org.mozilla.gecko.Element; |
|
20 import org.mozilla.gecko.FennecNativeActions; |
|
21 import org.mozilla.gecko.FennecNativeDriver; |
|
22 import org.mozilla.gecko.GeckoAppShell; |
|
23 import org.mozilla.gecko.GeckoEvent; |
|
24 import org.mozilla.gecko.GeckoThread; |
|
25 import org.mozilla.gecko.GeckoThread.LaunchState; |
|
26 import org.mozilla.gecko.R; |
|
27 import org.mozilla.gecko.RobocopUtils; |
|
28 import org.mozilla.gecko.Tab; |
|
29 import org.mozilla.gecko.Tabs; |
|
30 |
|
31 import android.app.Activity; |
|
32 import android.content.ContentValues; |
|
33 import android.content.Intent; |
|
34 import android.content.pm.ActivityInfo; |
|
35 import android.content.res.AssetManager; |
|
36 import android.database.Cursor; |
|
37 import android.os.Build; |
|
38 import android.os.SystemClock; |
|
39 import android.support.v4.app.Fragment; |
|
40 import android.support.v4.app.FragmentActivity; |
|
41 import android.support.v4.app.FragmentManager; |
|
42 import android.text.TextUtils; |
|
43 import android.util.DisplayMetrics; |
|
44 import android.view.View; |
|
45 import android.view.inputmethod.InputMethodManager; |
|
46 import android.widget.AdapterView; |
|
47 import android.widget.Button; |
|
48 import android.widget.EditText; |
|
49 import android.widget.ListAdapter; |
|
50 import android.widget.TextView; |
|
51 |
|
52 import com.jayway.android.robotium.solo.Condition; |
|
53 import com.jayway.android.robotium.solo.Solo; |
|
54 |
|
55 /** |
|
56 * A convenient base class suitable for most Robocop tests. |
|
57 */ |
|
58 @SuppressWarnings("unchecked") |
|
59 abstract class BaseTest extends BaseRobocopTest { |
|
60 private static final int VERIFY_URL_TIMEOUT = 2000; |
|
61 private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000; |
|
62 private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000; |
|
63 public static final int MAX_WAIT_MS = 4500; |
|
64 public static final int LONG_PRESS_TIME = 6000; |
|
65 private static final int GECKO_READY_WAIT_MS = 180000; |
|
66 public static final int MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS = 90000; |
|
67 |
|
68 private Activity mActivity; |
|
69 private int mPreferenceRequestID = 0; |
|
70 protected Solo mSolo; |
|
71 protected Driver mDriver; |
|
72 protected Actions mActions; |
|
73 protected String mBaseUrl; |
|
74 protected String mRawBaseUrl; |
|
75 protected String mProfile; |
|
76 public Device mDevice; |
|
77 protected DatabaseHelper mDatabaseHelper; |
|
78 protected StringHelper mStringHelper; |
|
79 protected int mScreenMidWidth; |
|
80 protected int mScreenMidHeight; |
|
81 |
|
82 protected void blockForGeckoReady() { |
|
83 try { |
|
84 Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:Ready"); |
|
85 if (!GeckoThread.checkLaunchState(LaunchState.GeckoRunning)) { |
|
86 geckoReadyExpector.blockForEvent(GECKO_READY_WAIT_MS, true); |
|
87 } |
|
88 geckoReadyExpector.unregisterListener(); |
|
89 } catch (Exception e) { |
|
90 mAsserter.dumpLog("Exception in blockForGeckoReady", e); |
|
91 } |
|
92 } |
|
93 |
|
94 @Override |
|
95 public void setUp() throws Exception { |
|
96 super.setUp(); |
|
97 |
|
98 // Create the intent to be used with all the important arguments. |
|
99 mBaseUrl = ((String) mConfig.get("host")).replaceAll("(/$)", ""); |
|
100 mRawBaseUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", ""); |
|
101 Intent i = new Intent(Intent.ACTION_MAIN); |
|
102 mProfile = (String) mConfig.get("profile"); |
|
103 i.putExtra("args", "-no-remote -profile " + mProfile); |
|
104 String envString = (String) mConfig.get("envvars"); |
|
105 if (envString != "") { |
|
106 String[] envStrings = envString.split(","); |
|
107 for (int iter = 0; iter < envStrings.length; iter++) { |
|
108 i.putExtra("env" + iter, envStrings[iter]); |
|
109 } |
|
110 } |
|
111 // Start the activity |
|
112 setActivityIntent(i); |
|
113 mActivity = getActivity(); |
|
114 // Set up Robotium.solo and Driver objects |
|
115 mSolo = new Solo(getInstrumentation(), mActivity); |
|
116 mDriver = new FennecNativeDriver(mActivity, mSolo, mRootPath); |
|
117 mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter); |
|
118 mDevice = new Device(); |
|
119 mDatabaseHelper = new DatabaseHelper(mActivity, mAsserter); |
|
120 mStringHelper = new StringHelper(); |
|
121 } |
|
122 |
|
123 @Override |
|
124 protected void runTest() throws Throwable { |
|
125 try { |
|
126 super.runTest(); |
|
127 } catch (Throwable t) { |
|
128 // save screenshot -- written to /mnt/sdcard/Robotium-Screenshots |
|
129 // as <filename>.jpg |
|
130 mSolo.takeScreenshot("robocop-screenshot"); |
|
131 if (mAsserter != null) { |
|
132 mAsserter.dumpLog("Exception caught during test!", t); |
|
133 mAsserter.ok(false, "Exception caught", t.toString()); |
|
134 } |
|
135 // re-throw to continue bail-out |
|
136 throw t; |
|
137 } |
|
138 } |
|
139 |
|
140 @Override |
|
141 public void tearDown() throws Exception { |
|
142 try { |
|
143 mAsserter.endTest(); |
|
144 // request a force quit of the browser and wait for it to take effect |
|
145 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null)); |
|
146 mSolo.sleep(7000); |
|
147 // if still running, finish activities as recommended by Robotium |
|
148 mSolo.finishOpenedActivities(); |
|
149 } catch (Throwable e) { |
|
150 e.printStackTrace(); |
|
151 } |
|
152 super.tearDown(); |
|
153 } |
|
154 |
|
155 public void assertMatches(String value, String regex, String name) { |
|
156 if (value == null) { |
|
157 mAsserter.ok(false, name, "Expected /" + regex + "/, got null"); |
|
158 return; |
|
159 } |
|
160 mAsserter.ok(value.matches(regex), name, "Expected /" + regex +"/, got \"" + value + "\""); |
|
161 } |
|
162 |
|
163 /** |
|
164 * Click on the URL bar to focus it and enter editing mode. |
|
165 */ |
|
166 protected final void focusUrlBar() { |
|
167 // Click on the browser toolbar to enter editing mode |
|
168 final View toolbarView = mSolo.getView(R.id.browser_toolbar); |
|
169 mSolo.clickOnView(toolbarView); |
|
170 |
|
171 // Wait for highlighed text to gain focus |
|
172 boolean success = waitForCondition(new Condition() { |
|
173 @Override |
|
174 public boolean isSatisfied() { |
|
175 EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text); |
|
176 if (urlEditText.isInputMethodTarget()) { |
|
177 return true; |
|
178 } |
|
179 return false; |
|
180 } |
|
181 }, MAX_WAIT_ENABLED_TEXT_MS); |
|
182 |
|
183 mAsserter.ok(success, "waiting for urlbar text to gain focus", "urlbar text gained focus"); |
|
184 } |
|
185 |
|
186 protected final void enterUrl(String url) { |
|
187 final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text); |
|
188 |
|
189 focusUrlBar(); |
|
190 |
|
191 // Send the keys for the URL we want to enter |
|
192 mSolo.clearEditText(urlEditView); |
|
193 mSolo.enterText(urlEditView, url); |
|
194 |
|
195 // Get the URL text from the URL bar EditText view |
|
196 final String urlBarText = urlEditView.getText().toString(); |
|
197 mAsserter.is(url, urlBarText, "URL typed properly"); |
|
198 } |
|
199 |
|
200 protected final Fragment getBrowserSearch() { |
|
201 final FragmentManager fm = ((FragmentActivity) getActivity()).getSupportFragmentManager(); |
|
202 return fm.findFragmentByTag("browser_search"); |
|
203 } |
|
204 |
|
205 protected final void hitEnterAndWait() { |
|
206 Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded"); |
|
207 mActions.sendSpecialKey(Actions.SpecialKey.ENTER); |
|
208 // wait for screen to load |
|
209 contentEventExpecter.blockForEvent(); |
|
210 contentEventExpecter.unregisterListener(); |
|
211 } |
|
212 |
|
213 /** |
|
214 * Load <code>url</code> by sending key strokes to the URL bar UI. |
|
215 * |
|
216 * This method waits synchronously for the <code>DOMContentLoaded</code> |
|
217 * message from Gecko before returning. |
|
218 */ |
|
219 protected final void inputAndLoadUrl(String url) { |
|
220 enterUrl(url); |
|
221 hitEnterAndWait(); |
|
222 } |
|
223 |
|
224 /** |
|
225 * Load <code>url</code> using reflection and the internal |
|
226 * <code>org.mozilla.gecko.Tabs</code> API. |
|
227 * |
|
228 * This method does not wait for any confirmation from Gecko before |
|
229 * returning. |
|
230 */ |
|
231 protected final void loadUrl(final String url) { |
|
232 try { |
|
233 Tabs.getInstance().loadUrl(url); |
|
234 } catch (Exception e) { |
|
235 mAsserter.dumpLog("Exception in loadUrl", e); |
|
236 throw new RuntimeException(e); |
|
237 } |
|
238 } |
|
239 |
|
240 protected final void closeTab(int tabId) { |
|
241 Tabs tabs = Tabs.getInstance(); |
|
242 Tab tab = tabs.getTab(tabId); |
|
243 tabs.closeTab(tab); |
|
244 } |
|
245 |
|
246 public final void verifyUrl(String url) { |
|
247 final EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text); |
|
248 String urlBarText = null; |
|
249 if (urlEditText != null) { |
|
250 // wait for a short time for the expected text, in case there is a delay |
|
251 // in updating the view |
|
252 waitForCondition(new VerifyTextViewText(urlEditText, url), VERIFY_URL_TIMEOUT); |
|
253 urlBarText = urlEditText.getText().toString(); |
|
254 |
|
255 } |
|
256 mAsserter.is(urlBarText, url, "Browser toolbar URL stayed the same"); |
|
257 } |
|
258 |
|
259 class VerifyTextViewText implements Condition { |
|
260 private TextView mTextView; |
|
261 private String mExpected; |
|
262 public VerifyTextViewText(TextView textView, String expected) { |
|
263 mTextView = textView; |
|
264 mExpected = expected; |
|
265 } |
|
266 |
|
267 @Override |
|
268 public boolean isSatisfied() { |
|
269 String textValue = mTextView.getText().toString(); |
|
270 return mExpected.equals(textValue); |
|
271 } |
|
272 } |
|
273 |
|
274 protected final String getAbsoluteUrl(String url) { |
|
275 return mBaseUrl + "/" + url.replaceAll("(^/)", ""); |
|
276 } |
|
277 |
|
278 protected final String getAbsoluteRawUrl(String url) { |
|
279 return mRawBaseUrl + "/" + url.replaceAll("(^/)", ""); |
|
280 } |
|
281 |
|
282 /* |
|
283 * Wrapper method for mSolo.waitForCondition with additional logging. |
|
284 */ |
|
285 protected final boolean waitForCondition(Condition condition, int timeout) { |
|
286 boolean result = mSolo.waitForCondition(condition, timeout); |
|
287 if (!result) { |
|
288 // Log timeout failure for diagnostic purposes only; a failed wait may |
|
289 // be normal and does not necessarily warrant a test asssertion/failure. |
|
290 mAsserter.dumpLog("waitForCondition timeout after " + timeout + " ms."); |
|
291 } |
|
292 return result; |
|
293 } |
|
294 |
|
295 // TODO: With Robotium 4.2, we should use Condition and waitForCondition instead. |
|
296 // Future boolean tests should not use this method. |
|
297 protected final boolean waitForTest(BooleanTest t, int timeout) { |
|
298 long end = SystemClock.uptimeMillis() + timeout; |
|
299 while (SystemClock.uptimeMillis() < end) { |
|
300 if (t.test()) { |
|
301 return true; |
|
302 } |
|
303 mSolo.sleep(100); |
|
304 } |
|
305 // log out wait failure for diagnostic purposes only; |
|
306 // a failed wait may be normal and does not necessarily |
|
307 // warrant a test assertion/failure |
|
308 mAsserter.dumpLog("waitForTest timeout after "+timeout+" ms"); |
|
309 return false; |
|
310 } |
|
311 |
|
312 // TODO: With Robotium 4.2, we should use Condition and waitForCondition instead. |
|
313 // Future boolean tests should not implement this interface. |
|
314 protected interface BooleanTest { |
|
315 public boolean test(); |
|
316 } |
|
317 |
|
318 public void SqliteCompare(String dbName, String sqlCommand, ContentValues[] cvs) { |
|
319 File profile = new File(mProfile); |
|
320 String dbPath = new File(profile, dbName).getPath(); |
|
321 |
|
322 Cursor c = mActions.querySql(dbPath, sqlCommand); |
|
323 SqliteCompare(c, cvs); |
|
324 } |
|
325 |
|
326 public void SqliteCompare(Cursor c, ContentValues[] cvs) { |
|
327 mAsserter.is(c.getCount(), cvs.length, "List is correct length"); |
|
328 if (c.moveToFirst()) { |
|
329 do { |
|
330 boolean found = false; |
|
331 for (int i = 0; !found && i < cvs.length; i++) { |
|
332 if (CursorMatches(c, cvs[i])) { |
|
333 found = true; |
|
334 } |
|
335 } |
|
336 mAsserter.is(found, true, "Password was found"); |
|
337 } while (c.moveToNext()); |
|
338 } |
|
339 } |
|
340 |
|
341 public boolean CursorMatches(Cursor c, ContentValues cv) { |
|
342 for (int i = 0; i < c.getColumnCount(); i++) { |
|
343 String column = c.getColumnName(i); |
|
344 if (cv.containsKey(column)) { |
|
345 mAsserter.info("Comparing", "Column values for: " + column); |
|
346 Object value = cv.get(column); |
|
347 if (value == null) { |
|
348 if (!c.isNull(i)) { |
|
349 return false; |
|
350 } |
|
351 } else { |
|
352 if (c.isNull(i) || !value.toString().equals(c.getString(i))) { |
|
353 return false; |
|
354 } |
|
355 } |
|
356 } |
|
357 } |
|
358 return true; |
|
359 } |
|
360 |
|
361 public InputStream getAsset(String filename) throws IOException { |
|
362 AssetManager assets = getInstrumentation().getContext().getAssets(); |
|
363 return assets.open(filename); |
|
364 } |
|
365 |
|
366 public boolean waitForText(String text) { |
|
367 boolean rc = mSolo.waitForText(text); |
|
368 if (!rc) { |
|
369 // log out failed wait for diagnostic purposes only; |
|
370 // waitForText failures are sometimes expected/normal |
|
371 mAsserter.dumpLog("waitForText timeout on "+text); |
|
372 } |
|
373 return rc; |
|
374 } |
|
375 |
|
376 // waitForText usually scrolls down in a view when text is not visible. |
|
377 // For PreferenceScreens and dialogs, Solo.waitForText scrolling does not |
|
378 // work, so we use this hack to do the same thing. |
|
379 protected boolean waitForPreferencesText(String txt) { |
|
380 boolean foundText = waitForText(txt); |
|
381 if (!foundText) { |
|
382 if ((mScreenMidWidth == 0) || (mScreenMidHeight == 0)) { |
|
383 mScreenMidWidth = mDriver.getGeckoWidth()/2; |
|
384 mScreenMidHeight = mDriver.getGeckoHeight()/2; |
|
385 } |
|
386 |
|
387 // If we don't see the item, scroll down once in case it's off-screen. |
|
388 // Hacky way to scroll down. solo.scroll* does not work in dialogs. |
|
389 MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop()); |
|
390 meh.dragSync(mScreenMidWidth, mScreenMidHeight+100, mScreenMidWidth, mScreenMidHeight-100); |
|
391 |
|
392 foundText = mSolo.waitForText(txt); |
|
393 } |
|
394 return foundText; |
|
395 } |
|
396 |
|
397 /** |
|
398 * Wait for <text> to be visible and also be enabled/clickable. |
|
399 */ |
|
400 public boolean waitForEnabledText(String text) { |
|
401 final String testText = text; |
|
402 boolean rc = waitForCondition(new Condition() { |
|
403 @Override |
|
404 public boolean isSatisfied() { |
|
405 // Solo.getText() could be used here, except that it sometimes |
|
406 // hits an assertion when the requested text is not found. |
|
407 ArrayList<View> views = mSolo.getCurrentViews(); |
|
408 for (View view : views) { |
|
409 if (view instanceof TextView) { |
|
410 TextView tv = (TextView)view; |
|
411 String viewText = tv.getText().toString(); |
|
412 if (tv.isEnabled() && viewText != null && viewText.matches(testText)) { |
|
413 return true; |
|
414 } |
|
415 } |
|
416 } |
|
417 return false; |
|
418 } |
|
419 }, MAX_WAIT_ENABLED_TEXT_MS); |
|
420 if (!rc) { |
|
421 // log out failed wait for diagnostic purposes only; |
|
422 // failures are sometimes expected/normal |
|
423 mAsserter.dumpLog("waitForEnabledText timeout on "+text); |
|
424 } |
|
425 return rc; |
|
426 } |
|
427 |
|
428 |
|
429 /** |
|
430 * Select <item> from Menu > "Settings" > <section>. |
|
431 */ |
|
432 public void selectSettingsItem(String section, String item) { |
|
433 String[] itemPath = { "Settings", section, item }; |
|
434 selectMenuItemByPath(itemPath); |
|
435 } |
|
436 |
|
437 /** |
|
438 * Traverses the items in listItems in order in the menu. |
|
439 */ |
|
440 public void selectMenuItemByPath(String[] listItems) { |
|
441 int listLength = listItems.length; |
|
442 if (listLength > 0) { |
|
443 selectMenuItem(listItems[0]); |
|
444 } |
|
445 if (listLength > 1) { |
|
446 for (int i = 1; i < listLength; i++) { |
|
447 String itemName = "^" + listItems[i] + "$"; |
|
448 mAsserter.ok(waitForPreferencesText(itemName), "Waiting for and scrolling once to find item " + itemName, itemName + " found"); |
|
449 mAsserter.ok(waitForEnabledText(itemName), "Waiting for enabled text " + itemName, itemName + " option is present and enabled"); |
|
450 mSolo.clickOnText(itemName); |
|
451 } |
|
452 } |
|
453 } |
|
454 |
|
455 public final void selectMenuItem(String menuItemName) { |
|
456 // build the item name ready to be used |
|
457 String itemName = "^" + menuItemName + "$"; |
|
458 mActions.sendSpecialKey(Actions.SpecialKey.MENU); |
|
459 if (waitForText(itemName)) { |
|
460 mSolo.clickOnText(itemName); |
|
461 } else { |
|
462 // Older versions of Android have additional settings under "More", |
|
463 // including settings that newer versions have under "Tools." |
|
464 if (mSolo.searchText("(^More$|^Tools$)")) { |
|
465 mSolo.clickOnText("(^More$|^Tools$)"); |
|
466 } |
|
467 waitForText(itemName); |
|
468 mSolo.clickOnText(itemName); |
|
469 } |
|
470 } |
|
471 |
|
472 public final void verifyHomePagerHidden() { |
|
473 final View homePagerContainer = mSolo.getView(R.id.home_pager_container); |
|
474 |
|
475 boolean rc = waitForCondition(new Condition() { |
|
476 @Override |
|
477 public boolean isSatisfied() { |
|
478 return homePagerContainer.getVisibility() != View.VISIBLE; |
|
479 } |
|
480 }, MAX_WAIT_HOME_PAGER_HIDDEN_MS); |
|
481 |
|
482 if (!rc) { |
|
483 mAsserter.ok(rc, "Verify HomePager is hidden", "HomePager is hidden"); |
|
484 } |
|
485 } |
|
486 |
|
487 public final void verifyPageTitle(String title) { |
|
488 final TextView urlBarTitle = (TextView) mSolo.getView(R.id.url_bar_title); |
|
489 String pageTitle = null; |
|
490 if (urlBarTitle != null) { |
|
491 // Wait for the title to make sure it has been displayed in case the view |
|
492 // does not update fast enough |
|
493 waitForCondition(new VerifyTextViewText(urlBarTitle, title), MAX_WAIT_MS); |
|
494 pageTitle = urlBarTitle.getText().toString(); |
|
495 } |
|
496 mAsserter.is(pageTitle, title, "Page title is correct"); |
|
497 } |
|
498 |
|
499 public final void verifyTabCount(int expectedTabCount) { |
|
500 Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter); |
|
501 String tabCountText = tabCount.getText(); |
|
502 int tabCountInt = Integer.parseInt(tabCountText); |
|
503 mAsserter.is(tabCountInt, expectedTabCount, "The correct number of tabs are opened"); |
|
504 } |
|
505 |
|
506 // Used to perform clicks on pop-up buttons without having to close the virtual keyboard |
|
507 public void clickOnButton(String label) { |
|
508 final Button button = mSolo.getButton(label); |
|
509 try { |
|
510 runTestOnUiThread(new Runnable() { |
|
511 @Override |
|
512 public void run() { |
|
513 button.performClick(); |
|
514 } |
|
515 }); |
|
516 } catch (Throwable throwable) { |
|
517 mAsserter.ok(false, "Unable to click the button","Was unable to click button "); |
|
518 } |
|
519 } |
|
520 |
|
521 // Used to hide/show the virtual keyboard |
|
522 public void toggleVKB() { |
|
523 InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE); |
|
524 imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0); |
|
525 } |
|
526 |
|
527 public void addTab() { |
|
528 mSolo.clickOnView(mSolo.getView(R.id.tabs)); |
|
529 // wait for addTab to appear (this is usually immediate) |
|
530 boolean success = waitForCondition(new Condition() { |
|
531 @Override |
|
532 public boolean isSatisfied() { |
|
533 View addTabView = mSolo.getView(R.id.add_tab); |
|
534 if (addTabView == null) { |
|
535 return false; |
|
536 } |
|
537 return true; |
|
538 } |
|
539 }, MAX_WAIT_MS); |
|
540 mAsserter.ok(success, "waiting for add tab view", "add tab view available"); |
|
541 Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); |
|
542 mSolo.clickOnView(mSolo.getView(R.id.add_tab)); |
|
543 pageShowExpecter.blockForEvent(); |
|
544 pageShowExpecter.unregisterListener(); |
|
545 } |
|
546 |
|
547 public void addTab(String url) { |
|
548 addTab(); |
|
549 |
|
550 // Adding a new tab opens about:home, so now we just need to load the url in it. |
|
551 inputAndLoadUrl(url); |
|
552 } |
|
553 |
|
554 /** |
|
555 * Gets the AdapterView of the tabs list. |
|
556 * |
|
557 * @return List view in the tabs tray |
|
558 */ |
|
559 private final AdapterView<ListAdapter> getTabsList() { |
|
560 Element tabs = mDriver.findElement(getActivity(), R.id.tabs); |
|
561 tabs.click(); |
|
562 return (AdapterView<ListAdapter>) getActivity().findViewById(R.id.normal_tabs); |
|
563 } |
|
564 |
|
565 /** |
|
566 * Gets the view in the tabs tray at the specified index. |
|
567 * |
|
568 * @return View at index |
|
569 */ |
|
570 private View getTabViewAt(final int index) { |
|
571 final View[] childView = { null }; |
|
572 |
|
573 final AdapterView<ListAdapter> view = getTabsList(); |
|
574 |
|
575 runOnUiThreadSync(new Runnable() { |
|
576 @Override |
|
577 public void run() { |
|
578 view.setSelection(index); |
|
579 |
|
580 // The selection isn't updated synchronously; posting a |
|
581 // runnable to the view's queue guarantees we'll run after the |
|
582 // layout pass. |
|
583 view.post(new Runnable() { |
|
584 @Override |
|
585 public void run() { |
|
586 // getChildAt() is relative to the list of visible |
|
587 // views, but our index is relative to all views in the |
|
588 // list. Subtract the first visible list position for |
|
589 // the correct offset. |
|
590 childView[0] = view.getChildAt(index - view.getFirstVisiblePosition()); |
|
591 } |
|
592 }); |
|
593 } |
|
594 }); |
|
595 |
|
596 boolean result = waitForCondition(new Condition() { |
|
597 @Override |
|
598 public boolean isSatisfied() { |
|
599 return childView[0] != null; |
|
600 } |
|
601 }, MAX_WAIT_MS); |
|
602 |
|
603 mAsserter.ok(result, "list item at index " + index + " exists", null); |
|
604 |
|
605 return childView[0]; |
|
606 } |
|
607 |
|
608 /** |
|
609 * Selects the tab at the specified index. |
|
610 * |
|
611 * @param index Index of tab to select |
|
612 */ |
|
613 public void selectTabAt(final int index) { |
|
614 mSolo.clickOnView(getTabViewAt(index)); |
|
615 } |
|
616 |
|
617 /** |
|
618 * Closes the tab at the specified index. |
|
619 * |
|
620 * @param index Index of tab to close |
|
621 */ |
|
622 public void closeTabAt(final int index) { |
|
623 View closeButton = getTabViewAt(index).findViewById(R.id.close); |
|
624 |
|
625 mSolo.clickOnView(closeButton); |
|
626 } |
|
627 |
|
628 public final void runOnUiThreadSync(Runnable runnable) { |
|
629 RobocopUtils.runOnUiThreadSync(mActivity, runnable); |
|
630 } |
|
631 |
|
632 /* Tap the "star" (bookmark) button to bookmark or un-bookmark the current page */ |
|
633 public void toggleBookmark() { |
|
634 mActions.sendSpecialKey(Actions.SpecialKey.MENU); |
|
635 waitForText("Settings"); |
|
636 |
|
637 // On ICS+ phones, there is no button labeled "Bookmarks" |
|
638 // instead we have to just dig through every button on the screen |
|
639 ArrayList<View> images = mSolo.getCurrentViews(); |
|
640 for (int i = 0; i < images.size(); i++) { |
|
641 final View view = images.get(i); |
|
642 boolean found = false; |
|
643 found = "Bookmark".equals(view.getContentDescription()); |
|
644 |
|
645 // on older android versions, try looking at the button's text |
|
646 if (!found) { |
|
647 if (view instanceof TextView) { |
|
648 found = "Bookmark".equals(((TextView)view).getText()); |
|
649 } |
|
650 } |
|
651 |
|
652 if (found) { |
|
653 int[] xy = new int[2]; |
|
654 view.getLocationOnScreen(xy); |
|
655 |
|
656 final int viewWidth = view.getWidth(); |
|
657 final int viewHeight = view.getHeight(); |
|
658 final float x = xy[0] + (viewWidth / 2.0f); |
|
659 float y = xy[1] + (viewHeight / 2.0f); |
|
660 |
|
661 mSolo.clickOnScreen(x, y); |
|
662 } |
|
663 } |
|
664 } |
|
665 |
|
666 public void clearPrivateData() { |
|
667 selectSettingsItem(StringHelper.PRIVACY_SECTION_LABEL, StringHelper.CLEAR_PRIVATE_DATA_LABEL); |
|
668 Actions.EventExpecter clearData = mActions.expectGeckoEvent("Sanitize:Finished"); |
|
669 mSolo.clickOnText("Clear data"); |
|
670 clearData.blockForEvent(); |
|
671 clearData.unregisterListener(); |
|
672 } |
|
673 |
|
674 class Device { |
|
675 public final String version; // 2.x or 3.x or 4.x |
|
676 public String type; // "tablet" or "phone" |
|
677 public final int width; |
|
678 public final int height; |
|
679 public final float density; |
|
680 |
|
681 public Device() { |
|
682 // Determine device version |
|
683 int sdk = Build.VERSION.SDK_INT; |
|
684 if (sdk < Build.VERSION_CODES.HONEYCOMB) { |
|
685 version = "2.x"; |
|
686 } else { |
|
687 if (sdk > Build.VERSION_CODES.HONEYCOMB_MR2) { |
|
688 version = "4.x"; |
|
689 } else { |
|
690 version = "3.x"; |
|
691 } |
|
692 } |
|
693 // Determine with and height |
|
694 DisplayMetrics dm = new DisplayMetrics(); |
|
695 getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm); |
|
696 height = dm.heightPixels; |
|
697 width = dm.widthPixels; |
|
698 density = dm.density; |
|
699 // Determine device type |
|
700 type = "phone"; |
|
701 try { |
|
702 if (GeckoAppShell.isTablet()) { |
|
703 type = "tablet"; |
|
704 } |
|
705 } catch (Exception e) { |
|
706 mAsserter.dumpLog("Exception in detectDevice", e); |
|
707 } |
|
708 } |
|
709 |
|
710 public void rotate() { |
|
711 if (getActivity().getRequestedOrientation () == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { |
|
712 mSolo.setActivityOrientation(Solo.PORTRAIT); |
|
713 } else { |
|
714 mSolo.setActivityOrientation(Solo.LANDSCAPE); |
|
715 } |
|
716 } |
|
717 } |
|
718 |
|
719 class Navigation { |
|
720 private String devType; |
|
721 private String osVersion; |
|
722 |
|
723 public Navigation(Device mDevice) { |
|
724 devType = mDevice.type; |
|
725 osVersion = mDevice.version; |
|
726 } |
|
727 |
|
728 public void back() { |
|
729 Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); |
|
730 |
|
731 if (devType.equals("tablet")) { |
|
732 Element backBtn = mDriver.findElement(getActivity(), R.id.back); |
|
733 backBtn.click(); |
|
734 } else { |
|
735 mActions.sendSpecialKey(Actions.SpecialKey.BACK); |
|
736 } |
|
737 |
|
738 pageShowExpecter.blockForEvent(); |
|
739 pageShowExpecter.unregisterListener(); |
|
740 } |
|
741 |
|
742 public void forward() { |
|
743 Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); |
|
744 |
|
745 if (devType.equals("tablet")) { |
|
746 Element fwdBtn = mDriver.findElement(getActivity(), R.id.forward); |
|
747 fwdBtn.click(); |
|
748 } else { |
|
749 mActions.sendSpecialKey(Actions.SpecialKey.MENU); |
|
750 waitForText("^New Tab$"); |
|
751 if (!osVersion.equals("2.x")) { |
|
752 Element fwdBtn = mDriver.findElement(getActivity(), R.id.forward); |
|
753 fwdBtn.click(); |
|
754 } else { |
|
755 mSolo.clickOnText("^Forward$"); |
|
756 } |
|
757 ensureMenuClosed(); |
|
758 } |
|
759 |
|
760 pageShowExpecter.blockForEvent(); |
|
761 pageShowExpecter.unregisterListener(); |
|
762 } |
|
763 |
|
764 public void reload() { |
|
765 if (devType.equals("tablet")) { |
|
766 Element reloadBtn = mDriver.findElement(getActivity(), R.id.reload); |
|
767 reloadBtn.click(); |
|
768 } else { |
|
769 mActions.sendSpecialKey(Actions.SpecialKey.MENU); |
|
770 waitForText("^New Tab$"); |
|
771 if (!osVersion.equals("2.x")) { |
|
772 Element reloadBtn = mDriver.findElement(getActivity(), R.id.reload); |
|
773 reloadBtn.click(); |
|
774 } else { |
|
775 mSolo.clickOnText("^Reload$"); |
|
776 } |
|
777 ensureMenuClosed(); |
|
778 } |
|
779 } |
|
780 |
|
781 // DEPRECATED! |
|
782 // Use BaseTest.toggleBookmark() in new code. |
|
783 public void bookmark() { |
|
784 mActions.sendSpecialKey(Actions.SpecialKey.MENU); |
|
785 waitForText("^New Tab$"); |
|
786 if (mSolo.searchText("^Bookmark$")) { |
|
787 // This is the Android 2.x so the button has text |
|
788 mSolo.clickOnText("^Bookmark$"); |
|
789 } else { |
|
790 Element bookmarkBtn = mDriver.findElement(getActivity(), R.id.bookmark); |
|
791 if (bookmarkBtn != null) { |
|
792 // We are on Android 4.x so the button is an image button |
|
793 bookmarkBtn.click(); |
|
794 } |
|
795 } |
|
796 ensureMenuClosed(); |
|
797 } |
|
798 |
|
799 // On some devices, the menu may not be dismissed after clicking on an |
|
800 // item. Close it here. |
|
801 private void ensureMenuClosed() { |
|
802 if (mSolo.searchText("^New Tab$")) { |
|
803 mActions.sendSpecialKey(Actions.SpecialKey.BACK); |
|
804 } |
|
805 } |
|
806 } |
|
807 |
|
808 /** |
|
809 * Gets the string representation of a stack trace. |
|
810 * |
|
811 * @param t Throwable to get stack trace for |
|
812 * @return Stack trace as a string |
|
813 */ |
|
814 public static String getStackTraceString(Throwable t) { |
|
815 StringWriter sw = new StringWriter(); |
|
816 t.printStackTrace(new PrintWriter(sw)); |
|
817 return sw.toString(); |
|
818 } |
|
819 |
|
820 /** |
|
821 * Condition class that waits for a view, and allows callers access it when done. |
|
822 */ |
|
823 private class DescriptionCondition<T extends View> implements Condition { |
|
824 public T mView; |
|
825 private String mDescr; |
|
826 private Class<T> mCls; |
|
827 |
|
828 public DescriptionCondition(Class<T> cls, String descr) { |
|
829 mDescr = descr; |
|
830 mCls = cls; |
|
831 } |
|
832 |
|
833 @Override |
|
834 public boolean isSatisfied() { |
|
835 mView = findViewWithContentDescription(mCls, mDescr); |
|
836 return (mView != null); |
|
837 } |
|
838 } |
|
839 |
|
840 /** |
|
841 * Wait for a view with the specified description . |
|
842 */ |
|
843 public <T extends View> T waitForViewWithDescription(Class<T> cls, String description) { |
|
844 DescriptionCondition<T> c = new DescriptionCondition<T>(cls, description); |
|
845 waitForCondition(c, MAX_WAIT_ENABLED_TEXT_MS); |
|
846 return c.mView; |
|
847 } |
|
848 |
|
849 /** |
|
850 * Get an active view with the specified description . |
|
851 */ |
|
852 public <T extends View> T findViewWithContentDescription(Class<T> cls, String description) { |
|
853 for (T view : mSolo.getCurrentViews(cls)) { |
|
854 final String descr = (String) view.getContentDescription(); |
|
855 if (TextUtils.isEmpty(descr)) { |
|
856 continue; |
|
857 } |
|
858 |
|
859 if (TextUtils.equals(description, descr)) { |
|
860 return view; |
|
861 } |
|
862 } |
|
863 |
|
864 return null; |
|
865 } |
|
866 |
|
867 /** |
|
868 * Abstract class for running small test cases within a BaseTest. |
|
869 */ |
|
870 abstract class TestCase implements Runnable { |
|
871 /** |
|
872 * Implement tests here. setUp and tearDown for the test case |
|
873 * should be handled by the parent test. This is so we can avoid the |
|
874 * overhead of starting Gecko and creating profiles. |
|
875 */ |
|
876 protected abstract void test() throws Exception; |
|
877 |
|
878 @Override |
|
879 public void run() { |
|
880 try { |
|
881 test(); |
|
882 } catch (Exception e) { |
|
883 mAsserter.ok(false, |
|
884 "Test " + this.getClass().getName() + " threw exception: " + e, |
|
885 ""); |
|
886 } |
|
887 } |
|
888 } |
|
889 |
|
890 /** |
|
891 * Set the preference and wait for it to change before proceeding with the test. |
|
892 */ |
|
893 public void setPreferenceAndWaitForChange(final JSONObject jsonPref) { |
|
894 mActions.sendGeckoEvent("Preferences:Set", jsonPref.toString()); |
|
895 |
|
896 // Get the preference name from the json and store it in an array. This array |
|
897 // will be used later while fetching the preference data. |
|
898 String[] prefNames = new String[1]; |
|
899 try { |
|
900 prefNames[0] = jsonPref.getString("name"); |
|
901 } catch (JSONException e) { |
|
902 mAsserter.ok(false, "Exception in setPreferenceAndWaitForChange", getStackTraceString(e)); |
|
903 } |
|
904 |
|
905 // Wait for confirmation of the pref change before proceeding with the test. |
|
906 final int ourRequestID = mPreferenceRequestID--; |
|
907 final Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data"); |
|
908 mActions.sendPreferencesGetEvent(ourRequestID, prefNames); |
|
909 |
|
910 // Wait until we get the correct "Preferences:Data" event |
|
911 waitForCondition(new Condition() { |
|
912 final long endTime = SystemClock.elapsedRealtime() + MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS; |
|
913 |
|
914 @Override |
|
915 public boolean isSatisfied() { |
|
916 try { |
|
917 long timeout = endTime - SystemClock.elapsedRealtime(); |
|
918 if (timeout < 0) { |
|
919 timeout = 0; |
|
920 } |
|
921 |
|
922 JSONObject data = new JSONObject(eventExpecter.blockForEventDataWithTimeout(timeout)); |
|
923 int requestID = data.getInt("requestId"); |
|
924 if (requestID != ourRequestID) { |
|
925 return false; |
|
926 } |
|
927 |
|
928 JSONArray preferences = data.getJSONArray("preferences"); |
|
929 mAsserter.is(preferences.length(), 1, "Expecting preference array to have one element"); |
|
930 JSONObject prefs = (JSONObject) preferences.get(0); |
|
931 mAsserter.is(prefs.getString("name"), jsonPref.getString("name"), |
|
932 "Expecting returned preference name to be the same as the set name"); |
|
933 mAsserter.is(prefs.getString("type"), jsonPref.getString("type"), |
|
934 "Expecting returned preference type to be the same as the set type"); |
|
935 mAsserter.is(prefs.get("value"), jsonPref.get("value"), |
|
936 "Expecting returned preference value to be the same as the set value"); |
|
937 return true; |
|
938 } catch(JSONException e) { |
|
939 mAsserter.ok(false, "Exception in setPreferenceAndWaitForChange", getStackTraceString(e)); |
|
940 // Please the java compiler |
|
941 return false; |
|
942 } |
|
943 } |
|
944 }, MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS); |
|
945 |
|
946 eventExpecter.unregisterListener(); |
|
947 } |
|
948 } |