diff options
Diffstat (limited to 'mobile/android/tests/browser/robocop/src/org/mozilla')
131 files changed, 19621 insertions, 0 deletions
diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java new file mode 100644 index 000000000..05e6bfa52 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Actions.java @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; +import android.database.Cursor; + +public interface Actions { + + /** Special keys supported by sendSpecialKey() */ + public enum SpecialKey { + DOWN, + UP, + LEFT, + RIGHT, + ENTER, + MENU, + DELETE, + } + + public interface EventExpecter { + /** Blocks until the event has been received. Subsequent calls will return immediately. */ + public void blockForEvent(); + public void blockForEvent(long millis, boolean failOnTimeout); + + /** Blocks until the event has been received and returns data associated with the event. */ + public String blockForEventData(); + + /** + * Blocks until the event has been received, or until the timeout has been exceeded. + * Returns the data associated with the event, if applicable. + */ + public String blockForEventDataWithTimeout(long millis); + + /** Polls to see if the event has been received. Once this returns true, subsequent calls will also return true. */ + public boolean eventReceived(); + + /** Stop listening for events. */ + public void unregisterListener(); + } + + public interface RepeatedEventExpecter extends EventExpecter { + /** Blocks until at least one event has been received, and no events have been received in the last <code>millis</code> milliseconds. */ + public void blockUntilClear(long millis); + } + + /** + * Sends an event to Gecko. + * + * @param geckoEvent The geckoEvent JSONObject's type + */ + void sendGeckoEvent(String geckoEvent, String data); + + public interface PrefWaiter { + boolean isFinished(); + void waitForFinish(); + void waitForFinish(long timeoutMillis, boolean failOnTimeout); + } + + public abstract static class PrefHandlerBase implements PrefsHelper.PrefHandler { + /* package */ Assert asserter; + + @Override // PrefsHelper.PrefHandler + public void prefValue(String pref, boolean value) { + asserter.ok(false, "Unexpected pref callback", ""); + } + + @Override // PrefsHelper.PrefHandler + public void prefValue(String pref, int value) { + asserter.ok(false, "Unexpected pref callback", ""); + } + + @Override // PrefsHelper.PrefHandler + public void prefValue(String pref, String value) { + asserter.ok(false, "Unexpected pref callback", ""); + } + + @Override // PrefsHelper.PrefHandler + public void finish() { + } + } + + PrefWaiter getPrefs(String[] prefNames, PrefHandlerBase handler); + void setPref(String pref, Object value, boolean flush); + PrefWaiter addPrefsObserver(String[] prefNames, PrefHandlerBase handler); + void removePrefsObserver(PrefWaiter handler); + + /** + * Listens for a gecko event to be sent from the Gecko instance. + * The returned object can be used to test if the event has been + * received. Note that only one event is listened for. + * + * @param geckoEvent The geckoEvent JSONObject's type + */ + RepeatedEventExpecter expectGeckoEvent(String geckoEvent); + + /** + * Listens for a paint event. Note that calling expectPaint() will + * invalidate the event expecters returned from any previous calls + * to expectPaint(); calling any methods on those invalidated objects + * will result in undefined behaviour. + */ + RepeatedEventExpecter expectPaint(); + + /** + * Send a string to the application + * + * @param keysToSend The string to send + */ + void sendKeys(String keysToSend); + + /** + * Send a special keycode to the element + * + * @param key The special key to send + */ + void sendSpecialKey(SpecialKey key); + void sendKeyCode(int keyCode); + + void drag(int startingX, int endingX, int startingY, int endingY); + + /** + * Run a sql query on the specified database + */ + public Cursor querySql(String dbPath, String sql); +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java new file mode 100644 index 000000000..aa76dcf2b --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Assert.java @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +public interface Assert { + void dumpLog(String message); + void dumpLog(String message, Throwable t); + void setLogFile(String filename); + void setTestName(String testName); + void endTest(); + + void ok(boolean condition, String name, String diag); + void is(Object actual, Object expected, String name); + void isnot(Object actual, Object notExpected, String name); + void todo(boolean condition, String name, String diag); + void todo_is(Object actual, Object expected, String name); + void todo_isnot(Object actual, Object notExpected, String name); + void info(String name, String message); + + // robocop-specific asserts + void ispixel(int actual, int r, int g, int b, String name); + void isnotpixel(int actual, int r, int g, int b, String name); +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java new file mode 100644 index 000000000..4c8373c5b --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Driver.java @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.app.Activity; + +public interface Driver { + /** + * Find the first Element using the given method. + * + * @param activity The activity the element belongs to + * @param id The resource id of the element + * @return The first matching element on the current context, or null if not found. + */ + Element findElement(Activity activity, int id); + + /** + * Sets up scroll handling so that data is received from the extension. + */ + void setupScrollHandling(); + + int getPageHeight(); + int getScrollHeight(); + int getHeight(); + int getGeckoTop(); + int getGeckoLeft(); + int getGeckoWidth(); + int getGeckoHeight(); + + void startFrameRecording(); + int stopFrameRecording(); + + void startCheckerboardRecording(); + float stopCheckerboardRecording(); + + /** + * Get a copy of the painted content region. + * @return A 2-D array of pixels (indexed by y, then x). The pixels + * are in ARGB-8888 format. + */ + PaintedSurface getPaintedSurface(); +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java new file mode 100644 index 000000000..97610ff32 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/Element.java @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +/** + * Element provides access to a specific UI view (android.view.View). + * See also Driver.findElement(). + */ +public interface Element { + + /** Click on the element's view. Returns true on success. */ + boolean click(); + + /** Returns true if the element is currently displayed */ + boolean isDisplayed(); + + /** + * Returns the text currently displayed on the element, or null + * if the text cannot be retrieved. + */ + String getText(); + + /** Returns the view ID */ + Integer getId(); +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java new file mode 100644 index 000000000..6bcb4e102 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecInstrumentationTestRunner.java @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.app.KeyguardManager; +import android.content.Context; +import android.os.Bundle; +import android.os.PowerManager; +import android.test.InstrumentationTestRunner; +import android.util.Log; + +import static android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP; +import static android.os.PowerManager.FULL_WAKE_LOCK; +import static android.os.PowerManager.ON_AFTER_RELEASE; + +public class FennecInstrumentationTestRunner extends InstrumentationTestRunner { + private static Bundle sArguments; + private PowerManager.WakeLock wakeLock; + private KeyguardManager.KeyguardLock keyguardLock; + + @Override + public void onCreate(Bundle arguments) { + sArguments = arguments; + if (sArguments == null) { + Log.e("Robocop", "FennecInstrumentationTestRunner.onCreate got null bundle"); + } + super.onCreate(arguments); + } + + // unfortunately we have to make this static because test classes that don't extend + // from ActivityInstrumentationTestCase2 can't get a reference to this class. + public static Bundle getFennecArguments() { + if (sArguments == null) { + Log.e("Robocop", "FennecInstrumentationTestCase.getFennecArguments returns null bundle"); + } + return sArguments; + } + + @Override + public void onStart() { + final Context context = getContext(); // The Robocop package itself has DISABLE_KEYGUARD and WAKE_LOCK. + if (context != null) { + try { + String name = FennecInstrumentationTestRunner.class.getSimpleName(); + // Unlock the device so that the tests can input keystrokes. + final KeyguardManager keyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + // Deprecated in favour of window flags, which aren't appropriate here. + keyguardLock = keyguard.newKeyguardLock(name); + keyguardLock.disableKeyguard(); + + // Wake up the screen. + final PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = power.newWakeLock(FULL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP | ON_AFTER_RELEASE, name); + wakeLock.acquire(); + } catch (SecurityException e) { + Log.w("GeckoInstTestRunner", "Got SecurityException: not disabling keyguard and not taking wakelock."); + } + } else { + Log.w("GeckoInstTestRunner", "Application target context is null: not disabling keyguard and not taking wakelock."); + } + + super.onStart(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (wakeLock != null) { + wakeLock.release(); + } + if (keyguardLock != null) { + // Deprecated in favour of window flags, which aren't appropriate here. + keyguardLock.reenableKeyguard(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java new file mode 100644 index 000000000..cb7c3c464 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecMochitestAssert.java @@ -0,0 +1,254 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import android.os.SystemClock; + +public class FennecMochitestAssert implements Assert { + // Internal state variables to make logging match up with existing mochitests + private int mPassed = 0; + private int mFailed = 0; + private int mTodo = 0; + + // Used to write the first line of the test file + private boolean mLogStarted = false; + + // Used to write the test-start/test-end log lines + private String mLogTestName = ""; + + // Measure the time it takes to run test case + private long mStartTime = 0; + + // Structured logger + private StructuredLogger mLogger; + + /** Write information to a logfile and logcat */ + public void dumpLog(String message) { + mLogger.info(message); + } + + public void dumpLog(String message, Throwable t) { + Writer sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + mLogger.error(message + " - " + sw.toString()); + } + + /** Write information to a logfile and logcat */ + static class DumpLogCallback implements StructuredLogger.LoggerCallback { + public void call(String output) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, output); + } + } + + + public FennecMochitestAssert() { + mLogger = new StructuredLogger("robocop", new DumpLogCallback()); + } + + /** Set the filename used for dumpLog. */ + public void setLogFile(String filename) { + FennecNativeDriver.setLogFile(filename); + + String message; + if (!mLogStarted) { + mLogger.info("SimpleTest START"); + mLogStarted = true; + } + + if (mLogTestName != "") { + long diff = SystemClock.uptimeMillis() - mStartTime; + mLogger.testEnd(mLogTestName, "OK", "finished in " + diff + "ms"); + } + } + + public void setTestName(String testName) { + String[] nameParts = testName.split("\\."); + mLogTestName = nameParts[nameParts.length - 1]; + mStartTime = SystemClock.uptimeMillis(); + + mLogger.testStart(mLogTestName); + } + + class testInfo { + public boolean mResult; + public String mName; + public String mDiag; + public boolean mTodo; + public boolean mInfo; + public testInfo(boolean r, String n, String d, boolean t, boolean i) { + mResult = r; + mName = n; + mDiag = d; + mTodo = t; + mInfo = i; + } + + } + + /** Used to log a subtest's result. + * test represents the subtest (an assertion). + * passStatus and passExpected are the actual status and the expected status if the assertion is true. + * failStatus and failExpected are the actual status and the expected status otherwise. + */ + private void _logMochitestResult(testInfo test, String passStatus, String passExpected, String failStatus, String failExpected) { + boolean isError = true; + if (test.mResult || test.mTodo) { + isError = false; + } + if (test.mResult) + { + mLogger.testStatus(mLogTestName, test.mName, passStatus, passExpected, test.mDiag); + } else { + mLogger.testStatus(mLogTestName, test.mName, failStatus, failExpected, test.mDiag); + } + + if (test.mInfo) { + // do not count TEST-INFO messages + } else if (test.mTodo) { + mTodo++; + } else if (isError) { + mFailed++; + } else { + mPassed++; + } + if (isError) { + String message = "TEST-UNEXPECTED-" + failStatus + " | " + mLogTestName + " | " + + test.mName + " - " + test.mDiag; + junit.framework.Assert.fail(message); + } + } + + public void endTest() { + String message; + + if (mLogTestName != "") { + long diff = SystemClock.uptimeMillis() - mStartTime; + mLogger.testEnd(mLogTestName, "OK", "finished in " + diff + "ms"); + } + + mLogger.info("TEST-START | Shutdown"); + mLogger.info("Passed: " + Integer.toString(mPassed)); + mLogger.info("Failed: " + Integer.toString(mFailed)); + mLogger.info("Todo: " + Integer.toString(mTodo)); + mLogger.info("SimpleTest FINISHED"); + } + + public void ok(boolean condition, String name, String diag) { + testInfo test = new testInfo(condition, name, diag, false, false); + _logMochitestResult(test, "PASS", "PASS", "FAIL", "PASS"); + } + + public void is(Object actual, Object expected, String name) { + boolean pass = checkObjectsEqual(actual, expected); + ok(pass, name, getEqualString(actual, expected, pass)); + } + + public void isnot(Object actual, Object notExpected, String name) { + boolean pass = checkObjectsNotEqual(actual, notExpected); + ok(pass, name, getNotEqualString(actual, notExpected, pass)); + } + + public void ispixel(int actual, int r, int g, int b, String name) { + int aAlpha = ((actual >> 24) & 0xFF); + int aR = ((actual >> 16) & 0xFF); + int aG = ((actual >> 8) & 0xFF); + int aB = (actual & 0xFF); + boolean pass = checkPixel(actual, r, g, b); + ok(pass, name, "Color rgba(" + aR + "," + aG + "," + aB + "," + aAlpha + ")" + (pass ? " " : " not") + " close enough to expected rgb(" + r + "," + g + "," + b + ")"); + } + + public void isnotpixel(int actual, int r, int g, int b, String name) { + int aAlpha = ((actual >> 24) & 0xFF); + int aR = ((actual >> 16) & 0xFF); + int aG = ((actual >> 8) & 0xFF); + int aB = (actual & 0xFF); + boolean pass = checkPixel(actual, r, g, b); + ok(!pass, name, "Color rgba(" + aR + "," + aG + "," + aB + "," + aAlpha + ")" + (!pass ? " is" : " is not") + " different enough from rgb(" + r + "," + g + "," + b + ")"); + } + + private boolean checkPixel(int actual, int r, int g, int b) { + // When we read GL pixels the GPU has already processed them and they + // are usually off by a little bit. For example a CSS-color pixel of color #64FFF5 + // was turned into #63FFF7 when it came out of glReadPixels. So in order to compare + // against the expected value, we use a little fuzz factor. For the alpha we just + // make sure it is always 0xFF. There is also bug 691354 which crops up every so + // often just to make our lives difficult. However the individual color components + // should never be off by more than 8. + int aAlpha = ((actual >> 24) & 0xFF); + int aR = ((actual >> 16) & 0xFF); + int aG = ((actual >> 8) & 0xFF); + int aB = (actual & 0xFF); + boolean pass = (aAlpha == 0xFF) /* alpha */ + && (Math.abs(aR - r) <= 8) /* red */ + && (Math.abs(aG - g) <= 8) /* green */ + && (Math.abs(aB - b) <= 8); /* blue */ + if (pass) { + return true; + } else { + return false; + } + } + + public void todo(boolean condition, String name, String diag) { + testInfo test = new testInfo(condition, name, diag, true, false); + _logMochitestResult(test, "PASS", "FAIL", "FAIL", "FAIL"); + } + + public void todo_is(Object actual, Object expected, String name) { + boolean pass = checkObjectsEqual(actual, expected); + todo(pass, name, getEqualString(actual, expected, pass)); + } + + public void todo_isnot(Object actual, Object notExpected, String name) { + boolean pass = checkObjectsNotEqual(actual, notExpected); + todo(pass, name, getNotEqualString(actual, notExpected, pass)); + } + + private boolean checkObjectsEqual(Object a, Object b) { + if (a == null || b == null) { + if (a == null && b == null) { + return true; + } + return false; + } else { + return a.equals(b); + } + } + + private String getEqualString(Object a, Object b, boolean pass) { + if (pass) { + return a + " should equal " + b; + } + return "got " + a + ", expected " + b; + } + + private boolean checkObjectsNotEqual(Object a, Object b) { + if (a == null || b == null) { + if ((a == null && b != null) || (a != null && b == null)) { + return true; + } else { + return false; + } + } else { + return !a.equals(b); + } + } + + private String getNotEqualString(Object a, Object b, boolean pass) { + if(pass) { + return a + " should not equal " + b; + } + return "didn't expect " + a + ", but got it"; + } + + public void info(String name, String message) { + mLogger.info(name + " | " + message); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java new file mode 100644 index 000000000..7faccdf43 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java @@ -0,0 +1,482 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.util.ArrayList; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.json.JSONObject; +import org.mozilla.gecko.FennecNativeDriver.LogLevel; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.gfx.LayerView.DrawListener; +import org.mozilla.gecko.mozglue.GeckoLoader; +import org.mozilla.gecko.sqlite.SQLiteBridge; +import org.mozilla.gecko.util.GeckoEventListener; + +import android.app.Activity; +import android.app.Instrumentation; +import android.database.Cursor; +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.KeyEvent; + +import com.robotium.solo.Solo; + +public class FennecNativeActions implements Actions { + private static final String LOGTAG = "FennecNativeActions"; + + private Solo mSolo; + private Instrumentation mInstr; + private Assert mAsserter; + + public FennecNativeActions(Activity activity, Solo robocop, Instrumentation instrumentation, Assert asserter) { + mSolo = robocop; + mInstr = instrumentation; + mAsserter = asserter; + + GeckoLoader.loadSQLiteLibs(activity, activity.getApplication().getPackageResourcePath()); + } + + class GeckoEventExpecter implements RepeatedEventExpecter { + private static final int MAX_WAIT_MS = 180000; + + private volatile boolean mIsRegistered; + + private final String mGeckoEvent; + private final GeckoEventListener mListener; + + private volatile boolean mEventEverReceived; + private String mEventData; + private BlockingQueue<String> mEventDataQueue; + + GeckoEventExpecter(final String geckoEvent) { + if (TextUtils.isEmpty(geckoEvent)) { + throw new IllegalArgumentException("geckoEvent must not be empty"); + } + + mGeckoEvent = geckoEvent; + mEventDataQueue = new LinkedBlockingQueue<String>(); + + final GeckoEventExpecter expecter = this; + mListener = new GeckoEventListener() { + @Override + public void handleMessage(final String event, final JSONObject message) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, + "handleMessage called for: " + event + "; expecting: " + mGeckoEvent); + mAsserter.is(event, mGeckoEvent, "Given message occurred for registered event: " + message); + + expecter.notifyOfEvent(message); + } + }; + + EventDispatcher.getInstance().registerGeckoThreadListener(mListener, mGeckoEvent); + mIsRegistered = true; + } + + public void blockForEvent() { + blockForEvent(MAX_WAIT_MS, true); + } + + public void blockForEvent(long millis, boolean failOnTimeout) { + if (!mIsRegistered) { + throw new IllegalStateException("listener not registered"); + } + + try { + mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + FennecNativeDriver.log(LogLevel.ERROR, ie); + } + if (mEventData == null) { + if (failOnTimeout) { + FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR); + mAsserter.ok(false, "GeckoEventExpecter", + "blockForEvent timeout: "+mGeckoEvent); + } else { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, + "blockForEvent timeout: "+mGeckoEvent); + } + } else { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, + "unblocked on expecter for " + mGeckoEvent); + } + } + + public void blockUntilClear(long millis) { + if (!mIsRegistered) { + throw new IllegalStateException("listener not registered"); + } + if (millis <= 0) { + throw new IllegalArgumentException("millis must be > 0"); + } + + // wait for at least one event + try { + mEventData = mEventDataQueue.poll(MAX_WAIT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + FennecNativeDriver.log(LogLevel.ERROR, ie); + } + if (mEventData == null) { + FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR); + mAsserter.ok(false, "GeckoEventExpecter", "blockUntilClear timeout"); + return; + } + // now wait for a period of millis where we don't get an event + while (true) { + try { + mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + FennecNativeDriver.log(LogLevel.INFO, ie); + } + if (mEventData == null) { + // success + break; + } + } + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, + "unblocked on expecter for " + mGeckoEvent); + } + + public String blockForEventData() { + blockForEvent(); + return mEventData; + } + + public String blockForEventDataWithTimeout(long millis) { + blockForEvent(millis, false); + return mEventData; + } + + public void unregisterListener() { + if (!mIsRegistered) { + throw new IllegalStateException("listener not registered"); + } + + FennecNativeDriver.log(LogLevel.INFO, + "EventExpecter: no longer listening for " + mGeckoEvent); + + EventDispatcher.getInstance().unregisterGeckoThreadListener(mListener, mGeckoEvent); + mIsRegistered = false; + } + + public boolean eventReceived() { + return mEventEverReceived; + } + + void notifyOfEvent(final JSONObject message) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, + "received event " + mGeckoEvent); + + mEventEverReceived = true; + + try { + mEventDataQueue.put(message.toString()); + } catch (InterruptedException e) { + FennecNativeDriver.log(LogLevel.ERROR, + "EventExpecter dropped event: " + message.toString(), e); + } + } + } + + public RepeatedEventExpecter expectGeckoEvent(final String geckoEvent) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, "waiting for " + geckoEvent); + return new GeckoEventExpecter(geckoEvent); + } + + public void sendGeckoEvent(final String geckoEvent, final String data) { + GeckoAppShell.notifyObservers(geckoEvent, data); + } + + public static final class PrefProxy implements PrefsHelper.PrefHandler, PrefWaiter { + public static final int MAX_WAIT_MS = 180000; + + /* package */ final PrefHandlerBase target; + private final String[] expectedPrefs; + private final ArrayList<String> seenPrefs = new ArrayList<>(); + private boolean finished = false; + + /* package */ PrefProxy(PrefHandlerBase target, String[] expectedPrefs, Assert asserter) { + this.target = target; + this.expectedPrefs = expectedPrefs; + target.asserter = asserter; + } + + @Override // PrefsHelper.PrefHandler + public void prefValue(String pref, boolean value) { + target.prefValue(pref, value); + seenPrefs.add(pref); + } + + @Override // PrefsHelper.PrefHandler + public void prefValue(String pref, int value) { + target.prefValue(pref, value); + seenPrefs.add(pref); + } + + @Override // PrefsHelper.PrefHandler + public void prefValue(String pref, String value) { + target.prefValue(pref, value); + seenPrefs.add(pref); + } + + @Override // PrefsHelper.PrefHandler + public synchronized void finish() { + target.finish(); + + for (String pref : expectedPrefs) { + target.asserter.ok(seenPrefs.remove(pref), "Checking pref was seen", pref); + } + target.asserter.ok(seenPrefs.isEmpty(), "Checking unexpected prefs", + TextUtils.join(", ", seenPrefs)); + + finished = true; + this.notifyAll(); + } + + @Override // PrefWaiter + public synchronized boolean isFinished() { + return finished; + } + + @Override // PrefWaiter + public void waitForFinish() { + waitForFinish(MAX_WAIT_MS, /* failOnTimeout */ true); + } + + @Override // PrefWaiter + public synchronized void waitForFinish(long timeoutMillis, boolean failOnTimeout) { + final long startTime = System.nanoTime(); + while (!finished) { + if (System.nanoTime() - startTime + >= timeoutMillis * 1e6 /* ns per ms */) { + final String prefsLog = "expected " + + TextUtils.join(", ", expectedPrefs) + "; got " + + TextUtils.join(", ", seenPrefs.toArray()) + "."; + if (failOnTimeout) { + FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR); + target.asserter.ok(false, "Timeout waiting for pref", prefsLog); + } else { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, + "Pref timeout (" + prefsLog + ")"); + } + break; + } + try { + this.wait(1000); // Wait for 1 second at a time. + } catch (final InterruptedException e) { + // Attempt waiting again. + } + } + finished = false; + } + } + + @Override // Actions + public PrefWaiter getPrefs(String[] prefNames, PrefHandlerBase handler) { + final PrefProxy proxy = new PrefProxy(handler, prefNames, mAsserter); + PrefsHelper.getPrefs(prefNames, proxy); + return proxy; + } + + @Override // Actions + public void setPref(String pref, Object value, boolean flush) { + PrefsHelper.setPref(pref, value, flush); + } + + @Override // Actions + public PrefWaiter addPrefsObserver(String[] prefNames, PrefHandlerBase handler) { + final PrefProxy proxy = new PrefProxy(handler, prefNames, mAsserter); + PrefsHelper.addObserver(prefNames, proxy); + return proxy; + } + + @Override // Actions + public void removePrefsObserver(PrefWaiter proxy) { + PrefsHelper.removeObserver((PrefProxy) proxy); + } + + class PaintExpecter implements RepeatedEventExpecter { + private static final int MAX_WAIT_MS = 90000; + + private boolean mPaintDone; + private boolean mListening; + + private final LayerView mLayerView; + private final DrawListener mDrawListener; + + PaintExpecter() { + final PaintExpecter expecter = this; + mLayerView = GeckoAppShell.getLayerView(); + mDrawListener = new DrawListener() { + @Override + public void drawFinished() { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, + "Received drawFinished notification"); + expecter.notifyOfEvent(); + } + }; + mLayerView.addDrawListener(mDrawListener); + mListening = true; + } + + private synchronized void notifyOfEvent() { + mPaintDone = true; + this.notifyAll(); + } + + public synchronized void blockForEvent(long millis, boolean failOnTimeout) { + if (!mListening) { + throw new IllegalStateException("draw listener not registered"); + } + long startTime = SystemClock.uptimeMillis(); + long endTime = 0; + while (!mPaintDone) { + try { + this.wait(millis); + } catch (InterruptedException ie) { + FennecNativeDriver.log(LogLevel.ERROR, ie); + break; + } + endTime = SystemClock.uptimeMillis(); + if (!mPaintDone && (endTime - startTime >= millis)) { + if (failOnTimeout) { + FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR); + mAsserter.ok(false, "PaintExpecter", "blockForEvent timeout"); + } + return; + } + } + } + + public synchronized void blockForEvent() { + blockForEvent(MAX_WAIT_MS, true); + } + + public synchronized String blockForEventData() { + blockForEvent(); + return null; + } + + public synchronized String blockForEventDataWithTimeout(long millis) { + blockForEvent(millis, false); + return null; + } + + public synchronized boolean eventReceived() { + return mPaintDone; + } + + public synchronized void blockUntilClear(long millis) { + if (!mListening) { + throw new IllegalStateException("draw listener not registered"); + } + if (millis <= 0) { + throw new IllegalArgumentException("millis must be > 0"); + } + // wait for at least one event + long startTime = SystemClock.uptimeMillis(); + long endTime = 0; + while (!mPaintDone) { + try { + this.wait(MAX_WAIT_MS); + } catch (InterruptedException ie) { + FennecNativeDriver.log(LogLevel.ERROR, ie); + break; + } + endTime = SystemClock.uptimeMillis(); + if (!mPaintDone && (endTime - startTime >= MAX_WAIT_MS)) { + FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR); + mAsserter.ok(false, "PaintExpecter", "blockUtilClear timeout"); + return; + } + } + // now wait for a period of millis where we don't get an event + startTime = SystemClock.uptimeMillis(); + while (true) { + try { + this.wait(millis); + } catch (InterruptedException ie) { + FennecNativeDriver.log(LogLevel.ERROR, ie); + break; + } + endTime = SystemClock.uptimeMillis(); + if (endTime - startTime >= millis) { + // success + break; + } + + // we got a notify() before we could wait long enough, so we need to start over + // Note, moving the goal post might have us race against a "drawFinished" flood + startTime = endTime; + } + } + + public synchronized void unregisterListener() { + if (!mListening) { + throw new IllegalStateException("listener not registered"); + } + + FennecNativeDriver.log(LogLevel.INFO, + "PaintExpecter: no longer listening for events"); + mLayerView.removeDrawListener(mDrawListener); + mListening = false; + } + } + + public RepeatedEventExpecter expectPaint() { + return new PaintExpecter(); + } + + public void sendSpecialKey(SpecialKey button) { + switch(button) { + case DOWN: + sendKeyCode(Solo.DOWN); + break; + case UP: + sendKeyCode(Solo.UP); + break; + case LEFT: + sendKeyCode(Solo.LEFT); + break; + case RIGHT: + sendKeyCode(Solo.RIGHT); + break; + case ENTER: + sendKeyCode(Solo.ENTER); + break; + case MENU: + sendKeyCode(Solo.MENU); + break; + case DELETE: + sendKeyCode(Solo.DELETE); + break; + default: + mAsserter.ok(false, "sendSpecialKey", "Unknown SpecialKey " + button); + break; + } + } + + public void sendKeyCode(int keyCode) { + if (keyCode <= 0 || keyCode > KeyEvent.getMaxKeyCode()) { + mAsserter.ok(false, "sendKeyCode", "Unknown keyCode " + keyCode); + } + mSolo.sendKey(keyCode); + } + + @Override + public void sendKeys(String input) { + mInstr.sendStringSync(input); + } + + public void drag(int startingX, int endingX, int startingY, int endingY) { + mSolo.drag(startingX, endingX, startingY, endingY, 10); + } + + public Cursor querySql(final String dbPath, final String sql) { + return new SQLiteBridge(dbPath).rawQuery(sql, null); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java new file mode 100644 index 000000000..3931b7e20 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeDriver.java @@ -0,0 +1,392 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.gfx.PanningPerfAPI; +import org.mozilla.gecko.util.GeckoEventListener; + +import android.app.Activity; +import android.util.Log; +import android.view.View; + +import com.robotium.solo.Solo; + +public class FennecNativeDriver implements Driver { + private static final int FRAME_TIME_THRESHOLD = 25; // allow 25ms per frame (40fps) + + private final Activity mActivity; + private final Solo mSolo; + private final String mRootPath; + + private static String mLogFile; + private static LogLevel mLogLevel = LogLevel.INFO; + + public enum LogLevel { + DEBUG(1), + INFO(2), + WARN(3), + ERROR(4); + + private final int mValue; + LogLevel(int value) { + mValue = value; + } + public boolean isEnabled(LogLevel configuredLevel) { + return mValue >= configuredLevel.getValue(); + } + private int getValue() { + return mValue; + } + } + + public FennecNativeDriver(Activity activity, Solo robocop, String rootPath) { + mActivity = activity; + mSolo = robocop; + mRootPath = rootPath; + } + + //Information on the location of the Gecko Frame. + private boolean mGeckoInfo = false; + private int mGeckoTop = 100; + private int mGeckoLeft = 0; + private int mGeckoHeight= 700; + private int mGeckoWidth = 1024; + + private void getGeckoInfo() { + View geckoLayout = mActivity.findViewById(R.id.gecko_layout); + if (geckoLayout != null) { + int[] pos = new int[2]; + geckoLayout.getLocationOnScreen(pos); + mGeckoTop = pos[1]; + mGeckoLeft = pos[0]; + mGeckoWidth = geckoLayout.getWidth(); + mGeckoHeight = geckoLayout.getHeight(); + mGeckoInfo = true; + } else { + throw new RoboCopException("Unable to find view gecko_layout"); + } + } + + @Override + public int getGeckoTop() { + if (!mGeckoInfo) { + getGeckoInfo(); + } + return mGeckoTop; + } + + @Override + public int getGeckoLeft() { + if (!mGeckoInfo) { + getGeckoInfo(); + } + return mGeckoLeft; + } + + @Override + public int getGeckoHeight() { + if (!mGeckoInfo) { + getGeckoInfo(); + } + return mGeckoHeight; + } + + @Override + public int getGeckoWidth() { + if (!mGeckoInfo) { + getGeckoInfo(); + } + return mGeckoWidth; + } + + /** Find the element with given id. + * + * @return An Element representing the view, or null if the view is not found. + */ + @Override + public Element findElement(Activity activity, int id) { + return new FennecNativeElement(id, activity); + } + + @Override + public void startFrameRecording() { + PanningPerfAPI.startFrameTimeRecording(); + } + + @Override + public int stopFrameRecording() { + final List<Long> frames = PanningPerfAPI.stopFrameTimeRecording(); + int badness = 0; + for (int i = 1; i < frames.size(); i++) { + long frameTime = frames.get(i) - frames.get(i - 1); + int delay = (int)(frameTime - FRAME_TIME_THRESHOLD); + // for each frame we miss, add the square of the delay. This + // makes large delays much worse than small delays. + if (delay > 0) { + badness += delay * delay; + } + } + + // Don't do any averaging of the numbers because really we want to + // know how bad the jank was at its worst + return badness; + } + + @Override + public void startCheckerboardRecording() { + PanningPerfAPI.startCheckerboardRecording(); + } + + @Override + public float stopCheckerboardRecording() { + final List<Float> checkerboard = PanningPerfAPI.stopCheckerboardRecording(); + float total = 0; + for (float val : checkerboard) { + total += val; + } + return total * 100.0f; + } + + private LayerView getSurfaceView() { + final LayerView layerView = mSolo.getView(LayerView.class, 0); + + if (layerView == null) { + log(LogLevel.WARN, "getSurfaceView could not find LayerView"); + for (final View v : mSolo.getViews()) { + log(LogLevel.WARN, " View: " + v); + } + } + return layerView; + } + + @Override + public PaintedSurface getPaintedSurface() { + final LayerView view = getSurfaceView(); + if (view == null) { + return null; + } + + final IntBuffer pixelBuffer = view.getPixels(); + + // now we need to (1) flip the image, because GL likes to do things up-side-down, + // and (2) rearrange the bits from AGBR-8888 to ARGB-8888. + int w = view.getWidth(); + int h = view.getHeight(); + pixelBuffer.position(0); + String mapFile = mRootPath + "/pixels.map"; + + FileOutputStream fos = null; + BufferedOutputStream bos = null; + DataOutputStream dos = null; + try { + fos = new FileOutputStream(mapFile); + bos = new BufferedOutputStream(fos); + dos = new DataOutputStream(bos); + + for (int y = h - 1; y >= 0; y--) { + for (int x = 0; x < w; x++) { + int agbr = pixelBuffer.get(); + dos.writeInt((agbr & 0xFF00FF00) | ((agbr >> 16) & 0x000000FF) | ((agbr << 16) & 0x00FF0000)); + } + } + } catch (IOException e) { + throw new RoboCopException("exception with pixel writer on file: " + mapFile); + } finally { + try { + if (dos != null) { + dos.flush(); + dos.close(); + } + // closing dos automatically closes bos + if (fos != null) { + fos.flush(); + fos.close(); + } + } catch (IOException e) { + log(LogLevel.ERROR, e); + throw new RoboCopException("exception closing pixel writer on file: " + mapFile); + } + } + return new PaintedSurface(mapFile, w, h); + } + + public int mHeight=0; + public int mScrollHeight=0; + public int mPageHeight=10; + + @Override + public int getScrollHeight() { + return mScrollHeight; + } + @Override + public int getPageHeight() { + return mPageHeight; + } + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void setupScrollHandling() { + GeckoApp.getEventDispatcher().registerGeckoThreadListener(new GeckoEventListener() { + @Override + public void handleMessage(final String event, final JSONObject message) { + try { + mScrollHeight = message.getInt("y"); + mHeight = message.getInt("cheight"); + // We don't want a height of 0. That means it's a bad response. + if (mHeight > 0) { + mPageHeight = message.getInt("height"); + } + } catch (JSONException e) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN, + "WARNING: ScrollReceived, but message does not contain " + + "expected fields: " + e); + } + } + }, "robocop:scroll"); + } + + /** + * Takes a filename, loads the file, and returns a string version of the entire file. + */ + public static String getFile(String filename) + { + StringBuilder text = new StringBuilder(); + + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader(filename)); + String line; + + while ((line = br.readLine()) != null) { + text.append(line); + text.append('\n'); + } + } catch (IOException e) { + log(LogLevel.ERROR, e); + } finally { + try { + if (br != null) { + br.close(); + } + } catch (IOException e) { + } + } + return text.toString(); + } + + /** + * Takes a string of "key=value" pairs split by \n and creates a hash table. + */ + public static Map<String, String> convertTextToTable(String data) + { + HashMap<String, String> retVal = new HashMap<String, String>(); + + String[] lines = data.split("\n"); + for (int i = 0; i < lines.length; i++) { + String[] parts = lines[i].split("=", 2); + retVal.put(parts[0].trim(), parts[1].trim()); + } + return retVal; + } + + public static void logAllStackTraces(LogLevel level) { + StringBuffer sb = new StringBuffer(); + sb.append("Dumping ALL the threads!\n"); + Map<Thread, StackTraceElement[]> allStacks = Thread.getAllStackTraces(); + for (Thread t : allStacks.keySet()) { + sb.append(t.toString()).append('\n'); + for (StackTraceElement ste : allStacks.get(t)) { + sb.append(ste.toString()).append('\n'); + } + sb.append('\n'); + } + log(level, sb.toString()); + } + + /** + * Set the filename used for logging. If the file already exists, delete it + * as a safe-guard against accidentally appending to an old log file. + */ + public static void setLogFile(String filename) { + mLogFile = filename; + File file = new File(mLogFile); + if (file.exists()) { + file.delete(); + } + } + + public static void setLogLevel(LogLevel level) { + mLogLevel = level; + } + + public static void log(LogLevel level, String message) { + log(level, message, null); + } + + public static void log(LogLevel level, Throwable t) { + log(level, null, t); + } + + public static void log(LogLevel level, String message, Throwable t) { + if (mLogFile == null) { + throw new RuntimeException("No log file specified!"); + } + + if (level.isEnabled(mLogLevel)) { + PrintWriter pw = null; + try { + pw = new PrintWriter(new FileWriter(mLogFile, true)); + if (message != null) { + pw.println(message); + } + if (t != null) { + t.printStackTrace(pw); + } + } catch (IOException ioe) { + Log.e("Robocop", "exception with file writer on: " + mLogFile); + } finally { + if (pw != null) { + pw.close(); + } + } + + // PrintWriter doesn't throw IOE but sets an error flag instead, + // so check for that + if (pw != null && pw.checkError()) { + Log.e("Robocop", "exception with file writer on: " + mLogFile); + } + } + + if (level == LogLevel.INFO) { + Log.i("Robocop", message, t); + } else if (level == LogLevel.DEBUG) { + Log.d("Robocop", message, t); + } else if (level == LogLevel.WARN) { + Log.w("Robocop", message, t); + } else if (level == LogLevel.ERROR) { + Log.e("Robocop", message, t); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java new file mode 100644 index 000000000..2a24344fd --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeElement.java @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextSwitcher; +import android.widget.TextView; + +public class FennecNativeElement implements Element { + private final Activity mActivity; + private final Integer mId; + private final String mName; + + public FennecNativeElement(Integer id, Activity activity) { + mId = id; + mActivity = activity; + mName = activity.getResources().getResourceName(id); + } + + @Override + public Integer getId() { + return mId; + } + + private boolean mClickSuccess; + + @Override + public boolean click() { + mClickSuccess = false; + RobocopUtils.runOnUiThreadSync(mActivity, + new Runnable() { + @Override + public void run() { + View view = mActivity.findViewById(mId); + if (view != null) { + if (view.performClick()) { + mClickSuccess = true; + } else { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN, + "Robocop called click on an element with no listener " + mId + " " + mName); + } + } else { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, + "click: unable to find view " + mId + " " + mName); + } + } + }); + return mClickSuccess; + } + + private Object mText; + + @Override + public String getText() { + mText = null; + RobocopUtils.runOnUiThreadSync(mActivity, + new Runnable() { + @Override + public void run() { + View v = mActivity.findViewById(mId); + if (v instanceof EditText) { + EditText et = (EditText)v; + mText = et.getEditableText(); + } else if (v instanceof TextSwitcher) { + TextSwitcher ts = (TextSwitcher)v; + mText = ((TextView)ts.getCurrentView()).getText(); + } else if (v instanceof ViewGroup) { + ViewGroup vg = (ViewGroup)v; + for (int i = 0; i < vg.getChildCount(); i++) { + if (vg.getChildAt(i) instanceof TextView) { + mText = ((TextView)vg.getChildAt(i)).getText(); + } + } + } else if (v instanceof TextView) { + mText = ((TextView)v).getText(); + } else if (v == null) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, + "getText: unable to find view " + mId + " " + mName); + } else { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, + "getText: unhandled type for view " + mId + " " + mName); + } + } // end of run() method definition + } // end of anonymous Runnable object instantiation + ); + if (mText == null) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN, + "getText: Text is null for view " + mId + " " + mName); + return null; + } + return mText.toString(); + } + + private boolean mDisplayed; + + @Override + public boolean isDisplayed() { + mDisplayed = false; + RobocopUtils.runOnUiThreadSync(mActivity, + new Runnable() { + @Override + public void run() { + View view = mActivity.findViewById(mId); + if (view != null) { + mDisplayed = true; + } + } + }); + return mDisplayed; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java new file mode 100644 index 000000000..862f66777 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecTalosAssert.java @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + + +public class FennecTalosAssert implements Assert { + + public FennecTalosAssert() { } + + /** + * Write information to a logfile and logcat + */ + public void dumpLog(String message) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, message); + } + + /** Write information to a logfile and logcat */ + public void dumpLog(String message, Throwable t) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.INFO, message, t); + } + + /** + * Set the filename used for dumpLog. + */ + public void setLogFile(String filename) { + FennecNativeDriver.setLogFile(filename); + } + + public void setTestName(String testName) { } + + public void endTest() { } + + public void ok(boolean condition, String name, String diag) { + if (!condition) { + dumpLog("__FAIL" + name + ": " + diag + "__FAIL"); + } + } + + public void is(Object actual, Object expected, String name) { + boolean pass = (actual == null ? expected == null : actual.equals(expected)); + ok(pass, name, "got " + actual + ", expected " + expected); + } + + public void isnot(Object actual, Object notExpected, String name) { + boolean fail = (actual == null ? notExpected == null : actual.equals(notExpected)); + ok(!fail, name, "got " + actual + ", expected not " + notExpected); + } + + public void ispixel(int actual, int r, int g, int b, String name) { + throw new UnsupportedOperationException(); + } + + public void isnotpixel(int actual, int r, int g, int b, String name) { + throw new UnsupportedOperationException(); + } + + public void todo(boolean condition, String name, String diag) { + throw new UnsupportedOperationException(); + } + + public void todo_is(Object actual, Object expected, String name) { + throw new UnsupportedOperationException(); + } + + public void todo_isnot(Object actual, Object notExpected, String name) { + throw new UnsupportedOperationException(); + } + + public void info(String name, String message) { + dumpLog(name + ": " + message); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java new file mode 100644 index 000000000..208b2c7bd --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/LaunchFennecWithConfigurationActivity.java @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.util.Map; + +import org.mozilla.gecko.tests.BaseRobocopTest; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +/** + * An Activity that extracts Robocop settings from robotium.config, launches + * Fennec with the Robocop testing parameters, and finishes itself. + * <p> + * This is intended to be used by local testers using |mach robocop --serve|. + */ +public class LaunchFennecWithConfigurationActivity extends Activity { + @Override + public void onCreate(Bundle arguments) { + super.onCreate(arguments); + } + + @Override + public void onResume() { + super.onResume(); + + final String configFile = FennecNativeDriver.getFile(BaseRobocopTest.DEFAULT_ROOT_PATH + "/robotium.config"); + final Map<String, String> config = FennecNativeDriver.convertTextToTable(configFile); + final Intent intent = BaseRobocopTest.createActivityIntent(config); + + intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); + + this.finish(); + this.startActivity(intent); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java new file mode 100644 index 000000000..17d77b758 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/PaintedSurface.java @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import android.graphics.Bitmap; +import android.util.Base64; +import android.util.Base64OutputStream; + +public class PaintedSurface { + private String mFileName; + private int mWidth; + private int mHeight; + private FileInputStream mPixelFile; + private MappedByteBuffer mPixelBuffer; + + public PaintedSurface(String filename, int width, int height) { + mFileName = filename; + mWidth = width; + mHeight = height; + + try { + File f = new File(filename); + int pixelSize = (int)f.length(); + + mPixelFile = new FileInputStream(filename); + mPixelBuffer = mPixelFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, pixelSize); + } catch (java.io.FileNotFoundException e) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e); + } catch (java.io.IOException e) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e); + } + } + + public final int getWidth() { + return mWidth; + } + + public final int getHeight() { + return mHeight; + } + + private int pixelAtIndex(int index) { + int b1 = mPixelBuffer.get(index) & 0xFF; + int b2 = mPixelBuffer.get(index + 1) & 0xFF; + int b3 = mPixelBuffer.get(index + 2) & 0xFF; + int b4 = mPixelBuffer.get(index + 3) & 0xFF; + int value = (b1 << 24) + (b2 << 16) + (b3 << 8) + (b4 << 0); + return value; + } + + public final int getPixelAt(int x, int y) { + if (mPixelBuffer == null) { + throw new RoboCopException("Trying to access PaintedSurface with no active PixelBuffer"); + } + + if (x >= mWidth || x < 0) { + throw new RoboCopException("Trying to access PaintedSurface with invalid x value"); + } + + if (y >= mHeight || y < 0) { + throw new RoboCopException("Trying to access PaintedSurface with invalid y value"); + } + + // The rows are reversed so row 0 is at the end and we start with the last row. + // This is why we do mHeight-y; + int index = (x + ((mHeight - y - 1) * mWidth)) * 4; + return pixelAtIndex(index); + } + + public final String asDataUri() { + try { + Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); + for (int y = 0; y < mHeight; y++) { + for (int x = 0; x < mWidth; x++) { + int index = (x + ((mHeight - y - 1) * mWidth)) * 4; + bm.setPixel(x, y, pixelAtIndex(index)); + } + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write("data:image/png;base64,".getBytes()); + Base64OutputStream b64 = new Base64OutputStream(out, Base64.NO_WRAP); + bm.compress(Bitmap.CompressFormat.PNG, 100, b64); + return new String(out.toByteArray()); + } catch (Exception e) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e); + throw new RoboCopException("Unable to convert surface to a PNG data:uri"); + } + } + + public void close() { + try { + mPixelFile.close(); + } catch (Exception e) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, e); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java new file mode 100644 index 000000000..420df818d --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RoboCopException.java @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +public class RoboCopException extends RuntimeException { + + public RoboCopException() { + super(); + } + + public RoboCopException(String message) { + super(message); + } + + public RoboCopException(Throwable cause) { + super(cause); + } + + public RoboCopException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java new file mode 100644 index 000000000..80ab3396c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare1.java @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +public class RobocopShare1 extends FragmentActivity { + private static Bundle sArguments; + + @Override + public void onCreate(Bundle arguments) { + super.onCreate(arguments); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java new file mode 100644 index 000000000..4874dffb7 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopShare2.java @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +public class RobocopShare2 extends FragmentActivity { + private static Bundle sArguments; + + @Override + public void onCreate(Bundle arguments) { + super.onCreate(arguments); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java new file mode 100644 index 000000000..7a33abfa6 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/RobocopUtils.java @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.util.concurrent.atomic.AtomicBoolean; + +import android.app.Activity; + +public final class RobocopUtils { + private static final int MAX_WAIT_MS = 20000; + + private RobocopUtils() {} + + public static void runOnUiThreadSync(Activity activity, final Runnable runnable) { + final AtomicBoolean sentinel = new AtomicBoolean(false); + + // On the UI thread, run the Runnable, then set sentinel to true and wake this thread. + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + runnable.run(); + + synchronized (sentinel) { + sentinel.set(true); + sentinel.notifyAll(); + } + } + } + ); + + + // Suspend this thread, until the other thread completes its work or until a timeout is + // reached. + long startTimestamp = System.currentTimeMillis(); + + synchronized (sentinel) { + while (!sentinel.get()) { + try { + sentinel.wait(MAX_WAIT_MS); + } catch (InterruptedException e) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, e); + } + + // Abort if we woke up due to timeout (instead of spuriously). + if (System.currentTimeMillis() - startTimestamp >= MAX_WAIT_MS) { + FennecNativeDriver.log(FennecNativeDriver.LogLevel.ERROR, + "time-out waiting for UI thread"); + FennecNativeDriver.logAllStackTraces(FennecNativeDriver.LogLevel.ERROR); + + return; + } + } + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java new file mode 100644 index 000000000..87d5a3c25 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/StructuredLogger.java @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.util.HashSet; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.json.JSONObject; + +// This implements the structured logging API described here: http://mozbase.readthedocs.org/en/latest/mozlog_structured.html +public class StructuredLogger { + private final static HashSet<String> validTestStatus = new HashSet<String>(Arrays.asList("PASS", "FAIL", "TIMEOUT", "NOTRUN", "ASSERT")); + private final static HashSet<String> validTestEnd = new HashSet<String>(Arrays.asList("PASS", "FAIL", "OK", "ERROR", "TIMEOUT", + "CRASH", "ASSERT", "SKIP")); + + private String mName; + private String mComponent; + private LoggerCallback mCallback; + + static public interface LoggerCallback { + public void call(String output); + } + + /* A default logger callback that prints the JSON output to stdout. + * This is not to be used in robocop as we write to a log file. */ + static class StandardLoggerCallback implements LoggerCallback { + public void call(String output) { + System.out.println(output); + } + } + + public StructuredLogger(String name, String component, LoggerCallback callback) { + mName = name; + mComponent = component; + mCallback = callback; + } + + public StructuredLogger(String name, String component) { + this(name, component, new StandardLoggerCallback()); + } + + public StructuredLogger(String name, LoggerCallback callback) { + this(name, null, callback); + } + + public StructuredLogger(String name) { + this(name, null, new StandardLoggerCallback()); + } + + public void suiteStart(List<String> tests, Map<String, Object> runInfo) { + HashMap<String, Object> data = new HashMap<String, Object>(); + data.put("tests", tests); + if (runInfo != null) { + data.put("run_info", runInfo); + } + this.logData("suite_start", data); + } + + public void suiteStart(List<String> tests) { + this.suiteStart(tests, null); + } + + public void suiteEnd() { + this.logData("suite_end"); + } + + public void testStart(String test) { + HashMap<String, Object> data = new HashMap<String, Object>(); + data.put("test", test); + this.logData("test_start", data); + } + + public void testStatus(String test, String subtest, String status, String expected, String message) { + status = status.toUpperCase(); + if (!StructuredLogger.validTestStatus.contains(status)) { + throw new IllegalArgumentException("Unrecognized status: " + status); + } + + HashMap<String, Object> data = new HashMap<String, Object>(); + data.put("test", test); + data.put("subtest", subtest); + data.put("status", status); + + if (message != null) { + data.put("message", message); + } + if (!expected.equals(status)) { + data.put("expected", expected); + } + + this.logData("test_status", data); + } + + public void testStatus(String test, String subtest, String status, String message) { + this.testStatus(test, subtest, status, "PASS", message); + } + + public void testEnd(String test, String status, String expected, String message, Map<String, Object> extra) { + status = status.toUpperCase(); + if (!StructuredLogger.validTestEnd.contains(status)) { + throw new IllegalArgumentException("Unrecognized status: " + status); + } + + HashMap<String, Object> data = new HashMap<String, Object>(); + data.put("test", test); + data.put("status", status); + + if (message != null) { + data.put("message", message); + } + if (extra != null) { + data.put("extra", extra); + } + if (!expected.equals(status) && !status.equals("SKIP")) { + data.put("expected", expected); + } + + this.logData("test_end", data); + } + + public void testEnd(String test, String status, String expected, String message) { + this.testEnd(test, status, expected, message, null); + } + + public void testEnd(String test, String status, String message) { + this.testEnd(test, status, "OK", message, null); + } + + + public void debug(String message) { + this.log("debug", message); + } + + public void info(String message) { + this.log("info", message); + } + + public void warning(String message) { + this.log("warning", message); + } + + public void error(String message) { + this.log("error", message); + } + + public void critical(String message) { + this.log("critical", message); + } + + private void log(String level, String message) { + HashMap<String, Object> data = new HashMap<String, Object>(); + data.put("message", message); + data.put("level", level); + this.logData("log", data); + } + + private HashMap<String, Object> makeLogData(String action, Map<String, Object> data) { + HashMap<String, Object> allData = new HashMap<String, Object>(); + allData.put("action", action); + allData.put("time", System.currentTimeMillis()); + allData.put("thread", JSONObject.NULL); + allData.put("pid", JSONObject.NULL); + allData.put("source", mName); + if (mComponent != null) { + allData.put("component", mComponent); + } + + allData.putAll(data); + + return allData; + } + + private void logData(String action, Map<String, Object> data) { + HashMap<String, Object> logData = this.makeLogData(action, data); + JSONObject jsonObject = new JSONObject(logData); + mCallback.call(jsonObject.toString()); + } + + private void logData(String action) { + this.logData(action, new HashMap<String, Object>()); + } + +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java new file mode 100644 index 000000000..cadb5df93 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.ArrayList; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.home.HomePager; + +import android.support.v4.view.ViewPager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TabWidget; +import android.widget.TextView; + +import com.robotium.solo.Condition; + +/** + * This class is an extension of BaseTest that helps with interaction with about:home + * This class contains methods that access the different tabs from about:home, methods that get information like history and bookmarks from the database, edit and remove bookmarks and history items + * The purpose of this class is to collect all the logically connected methods that deal with about:home + * To use any of these methods in your test make sure it extends AboutHomeTest instead of BaseTest + */ +abstract class AboutHomeTest extends PixelTest { + protected enum AboutHomeTabs { + RECENT_TABS, + HISTORY, + TOP_SITES, + BOOKMARKS, + }; + + private final ArrayList<String> aboutHomeTabs = new ArrayList<String>() {{ + add("TOP_SITES"); + add("BOOKMARKS"); + }}; + + + @Override + public void setUp() throws Exception { + super.setUp(); + + if (aboutHomeTabs.size() < 4) { + // Update it for tablets vs. phones. + if (mDevice.type.equals("phone")) { + aboutHomeTabs.add(0, AboutHomeTabs.HISTORY.toString()); + aboutHomeTabs.add(0, AboutHomeTabs.RECENT_TABS.toString()); + } else { + aboutHomeTabs.add(AboutHomeTabs.HISTORY.toString()); + aboutHomeTabs.add(AboutHomeTabs.RECENT_TABS.toString()); + } + } + } + + /** + * FIXME: Write new versions of these methods and update their consumers to use the new about:home pages. + */ + protected ListView getHistoryList(String waitText, int expectedChildCount) { + return null; + } + protected ListView getHistoryList(String waitText) { + return null; + } + + // Returns true if the bookmark is displayed in the bookmarks tab, false otherwise - does not check in folders + protected void isBookmarkDisplayed(final String url) { + boolean isCorrect = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + View bookmark = getDisplayedBookmark(url); + return bookmark != null; + } + }, MAX_WAIT_MS); + + mAsserter.ok(isCorrect, "Checking that " + url + " displayed as a bookmark", url + " displayed"); + } + + // Loads a bookmark by tapping on the bookmark view in the Bookmarks tab + protected void loadBookmark(String url) { + View bookmark = getDisplayedBookmark(url); + if (bookmark != null) { + Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded"); + mSolo.clickOnView(bookmark); + contentEventExpecter.blockForEvent(); + contentEventExpecter.unregisterListener(); + } else { + mAsserter.ok(false, url + " is not one of the displayed bookmarks", "Please make sure the url provided is bookmarked"); + } + } + + // Opens the bookmark context menu by long-tapping on it + protected void openBookmarkContextMenu(String url) { + View bookmark = getDisplayedBookmark(url); + if (bookmark != null) { + mSolo.waitForView(bookmark); + mSolo.clickLongOnView(bookmark, LONG_PRESS_TIME); + mSolo.waitForDialogToOpen(); + } else { + mAsserter.ok(false, url + " is not one of the displayed bookmarks", "Please make sure the url provided is bookmarked"); + } + } + + // @return the View associated with bookmark for the provided url or null if the link is not bookmarked + protected View getDisplayedBookmark(String url) { + openAboutHomeTab(AboutHomeTabs.BOOKMARKS); + mSolo.hideSoftKeyboard(); + getInstrumentation().waitForIdleSync(); + ListView bookmarksTabList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS); + waitForNonEmptyListToLoad(bookmarksTabList); + ListAdapter adapter = bookmarksTabList.getAdapter(); + if (adapter != null) { + for (int i = 0; i < adapter.getCount(); i++ ) { + // I am unable to click the view taken with getView for some reason so getting the child at i + bookmarksTabList.smoothScrollToPosition(i); + View bookmarkView = bookmarksTabList.getChildAt(i); + if (bookmarkView instanceof android.widget.LinearLayout) { + ViewGroup bookmarkItemView = (ViewGroup) bookmarkView; + for (int j = 0 ; j < bookmarkItemView.getChildCount(); j++) { + View bookmarkContent = bookmarkItemView.getChildAt(j); + if (bookmarkContent instanceof android.widget.LinearLayout) { + ViewGroup bookmarkItemLayout = (ViewGroup) bookmarkContent; + for (int k = 0 ; k < bookmarkItemLayout.getChildCount(); k++) { + // Both the title and url are represented as text views so we can cast the view without any issues + TextView bookmarkTextContent = (TextView)bookmarkItemLayout.getChildAt(k); + if (url.equals(bookmarkTextContent.getText().toString())) { + return bookmarkView; + } + } + } + } + } + } + } + return null; + } + + /** + * Waits for the given ListView to have a non-empty adapter and be populated + * with a minimum number of items. + * + * This method will return false if the given ListView or its adapter is null, + * or if the ListView does not have the minimum number of items. + */ + protected boolean waitForListToLoad(final ListView listView, final int minSize) { + Condition listWaitCondition = new Condition() { + @Override + public boolean isSatisfied() { + if (listView == null) { + return false; + } + + final ListAdapter adapter = listView.getAdapter(); + if (adapter == null) { + return false; + } + + return (listView.getCount() - listView.getHeaderViewsCount() >= minSize); + } + }; + return waitForCondition(listWaitCondition, MAX_WAIT_MS); + } + + protected boolean waitForNonEmptyListToLoad(final ListView listView) { + return waitForListToLoad(listView, 1); + } + + /** + * Get an active ListView with the specified tag . + * + * This method uses the predefined tags in HomePager. + */ + protected final ListView findListViewWithTag(String tag) { + for (ListView listView : mSolo.getCurrentViews(ListView.class)) { + final String listTag = (String) listView.getTag(); + if (TextUtils.isEmpty(listTag)) { + continue; + } + + if (TextUtils.equals(listTag, tag)) { + return listView; + } + } + + return null; + } + + // A wait in order for the about:home tab to be rendered after drag/tab selection + private void waitForAboutHomeTab(final int tabIndex) { + boolean correctTab = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + ViewPager pager = mSolo.getView(ViewPager.class, 0); + return (pager.getCurrentItem() == tabIndex); + } + }, MAX_WAIT_MS); + mAsserter.ok(correctTab, "Checking that the correct tab is displayed", "The " + aboutHomeTabs.get(tabIndex) + " tab is displayed"); + } + + private void clickAboutHomeTab(AboutHomeTabs tab) { + mSolo.clickOnText(tab.toString().replace("_", " ")); + } + + /** + * Swipes to an about:home tab. + * @param swipeVector swipeVector Value and direction to swipe (go left for negative, right for positive). + */ + private void swipeAboutHome(int swipeVector) { + // Increase swipe width, which will especially impact tablets. + int swipeWidth = mDriver.getGeckoWidth() - 1; + int swipeHeight = mDriver.getGeckoHeight() / 2; + + if (swipeVector >= 0) { + // Emulate swipe motion from right to left. + for (int i = 0; i < swipeVector; i++) { + mActions.drag(swipeWidth, 0, swipeHeight, swipeHeight); + mSolo.sleep(100); + } + } else { + // Emulate swipe motion from left to right. + for (int i = 0; i > swipeVector; i--) { + mActions.drag(0, swipeWidth, swipeHeight, swipeHeight); + mSolo.sleep(100); + } + } + } + + /** + * This method can be used to open the different tabs of about:home. + */ + protected void openAboutHomeTab(AboutHomeTabs tab) { + focusUrlBar(); + ViewPager pager = mSolo.getView(ViewPager.class, 0); + final int currentTabIndex = pager.getCurrentItem(); + int tabOffset; + + // Handle tablets by just clicking the visible tab title. + if (mDevice.type.equals("tablet")) { + clickAboutHomeTab(tab); + return; + } + + // Handle phones (non-tablets). + tabOffset = aboutHomeTabs.indexOf(tab.toString()) - currentTabIndex; + swipeAboutHome(tabOffset); + waitForAboutHomeTab(aboutHomeTabs.indexOf(tab.toString())); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java new file mode 100644 index 000000000..3033524e8 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java @@ -0,0 +1,288 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.test.ActivityInstrumentationTestCase2; +import android.text.TextUtils; +import android.util.Log; + +import com.robotium.solo.Solo; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.Assert; +import org.mozilla.gecko.BrowserApp; +import org.mozilla.gecko.Driver; +import org.mozilla.gecko.FennecInstrumentationTestRunner; +import org.mozilla.gecko.FennecMochitestAssert; +import org.mozilla.gecko.FennecNativeActions; +import org.mozilla.gecko.FennecNativeDriver; +import org.mozilla.gecko.FennecTalosAssert; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.updater.UpdateServiceHelper; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; + +@SuppressWarnings("unchecked") +public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> { + public static final String LOGTAG = "BaseTest"; + + public enum Type { + MOCHITEST, + TALOS + } + + public static final String DEFAULT_ROOT_PATH = "/mnt/sdcard/tests"; + + // How long to wait for a Robocop:Quit message to actually kill Fennec. + private static final int ROBOCOP_QUIT_WAIT_MS = 180000; + + /** + * The Java Class instance that launches the browser. + * <p> + * This should always agree with {@link AppConstants#MOZ_ANDROID_BROWSER_INTENT_CLASS}. + */ + public static final Class<? extends Activity> BROWSER_INTENT_CLASS; + + // Use reflection here so we don't have to preprocess this file. + static { + Class<? extends Activity> cl; + try { + cl = (Class<? extends Activity>) Class.forName(AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS); + } catch (ClassNotFoundException e) { + // Oh well. + cl = Activity.class; + } + BROWSER_INTENT_CLASS = cl; + } + + protected Assert mAsserter; + protected String mLogFile; + + protected String mBaseHostnameUrl; + protected String mBaseIpUrl; + + protected Map<String, String> mConfig; + protected String mRootPath; + + protected Solo mSolo; + protected Driver mDriver; + protected Actions mActions; + + protected String mProfile; + + protected StringHelper mStringHelper; + + /** + * The browser is started at the beginning of this test. A single test is a + * class inheriting from <code>BaseRobocopTest</code> that contains test + * methods. + * <p> + * If a test should not start the browser at the beginning of a test, + * specify a different activity class to the one-argument constructor. To do + * as little as possible, specify <code>Activity.class</code>. + */ + public BaseRobocopTest() { + this((Class<Activity>) BROWSER_INTENT_CLASS); + } + + /** + * Start the given activity class at the beginning of this test. + * <p> + * <b>You should use the no-argument constructor in almost all cases.</b> + * + * @param activityClass to start before this test. + */ + protected BaseRobocopTest(Class<Activity> activityClass) { + super(activityClass); + } + + /** + * Returns the test type: mochitest or talos. + * <p> + * By default tests are mochitests, but a test can override this method in + * order to change its type. Most Robocop tests are mochitests. + */ + protected Type getTestType() { + return Type.MOCHITEST; + } + + // Member function to allow specialization. + protected Intent createActivityIntent() { + return BaseRobocopTest.createActivityIntent(mConfig); + } + + // Static function to allow re-use. + public static Intent createActivityIntent(Map<String, String> config) { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.putExtra("args", "-no-remote -profile " + config.get("profile")); + // Don't show the first run experience. + intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true); + + final String envString = config.get("envvars"); + if (!TextUtils.isEmpty(envString)) { + final String[] envStrings = envString.split(","); + + for (int iter = 0; iter < envStrings.length; iter++) { + intent.putExtra("env" + iter, envStrings[iter]); + } + } + + return intent; + } + + @Override + protected void setUp() throws Exception { + // Disable the updater. + UpdateServiceHelper.setEnabled(false); + + // Load config file from root path (set up by Python script). + mRootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot"); + if (mRootPath == null) { + Log.w("Robocop", "Did not find deviceroot in arguments; falling back to: " + DEFAULT_ROOT_PATH); + mRootPath = DEFAULT_ROOT_PATH; + } + String configFile = FennecNativeDriver.getFile(mRootPath + "/robotium.config"); + mConfig = FennecNativeDriver.convertTextToTable(configFile); + mLogFile = mConfig.get("logfile"); + mProfile = mConfig.get("profile"); + mBaseHostnameUrl = mConfig.get("host").replaceAll("(/$)", ""); + mBaseIpUrl = mConfig.get("rawhost").replaceAll("(/$)", ""); + + // Initialize the asserter. + if (getTestType() == Type.TALOS) { + mAsserter = new FennecTalosAssert(); + } else { + mAsserter = new FennecMochitestAssert(); + } + mAsserter.setLogFile(mLogFile); + mAsserter.setTestName(getClass().getName()); + + // Start the activity. + final Intent intent = createActivityIntent(); + setActivityIntent(intent); + + // Set up Robotium.solo and Driver objects + Activity tempActivity = getActivity(); + + StringHelper.initialize(tempActivity.getResources()); + mStringHelper = StringHelper.get(); + + mSolo = new Solo(getInstrumentation(), tempActivity); + mDriver = new FennecNativeDriver(tempActivity, mSolo, mRootPath); + mActions = new FennecNativeActions(tempActivity, mSolo, getInstrumentation(), mAsserter); + } + + @Override + protected void runTest() throws Throwable { + try { + super.runTest(); + } catch (Throwable t) { + // save screenshot -- written to /mnt/sdcard/Robotium-Screenshots + // as <filename>.jpg + mSolo.takeScreenshot("robocop-screenshot-"+getClass().getName()); + if (mAsserter != null) { + mAsserter.dumpLog("Exception caught during test!", t); + mAsserter.ok(false, "Exception caught", t.toString()); + } + // re-throw to continue bail-out + throw t; + } + } + + @Override + public void tearDown() throws Exception { + try { + mAsserter.endTest(); + + // By default, we don't quit Fennec on finish, and we don't finish + // all opened activities. Not quiting Fennec entirely is intended to + // make life better for local testers, who might want to alter a + // test that is under development rather than Fennec itself. Not + // finishing activities is intended to allow local testers to + // manually inspect an activity's state after a test + // run. runtestsremote.py sets this to "1". Testers running via an + // IDE will not have this set at all. + final String quitAndFinish = FennecInstrumentationTestRunner.getFennecArguments() + .getString("quit_and_finish"); // null means not specified. + if ("1".equals(quitAndFinish)) { + // Request the browser force quit and wait for it to take effect. + Log.i(LOGTAG, "Requesting force quit."); + mActions.sendGeckoEvent("Robocop:Quit", null); + mSolo.sleep(ROBOCOP_QUIT_WAIT_MS); + + // If still running, finish activities as recommended by Robotium. + Log.i(LOGTAG, "Finishing all opened activities."); + mSolo.finishOpenedActivities(); + } else { + // This has the effect of keeping the activity-under-test + // around; if we don't set it to null, it is killed, either by + // finishOpenedActivities above or super.tearDown below. + Log.i(LOGTAG, "Not requesting force quit and trying to keep started activity alive."); + setActivity(null); + } + } catch (Throwable e) { + e.printStackTrace(); + } + super.tearDown(); + } + + /** + * Function to early abort if we can't reach the given HTTP server. Provides local testers + * with diagnostic information. Not currently available for TALOS tests, which are rarely run + * locally in any case. + */ + public void throwIfHttpGetFails() { + if (getTestType() == Type.TALOS) { + return; + } + + // rawURL to test fetching from. This should be a raw (IP) URL, not an alias + // (like mochi.test). We can't (easily) test fetching from the aliases, since + // those are managed by Fennec's proxy settings. + final String rawUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", ""); + + HttpURLConnection urlConnection = null; + + try { + urlConnection = (HttpURLConnection) new URL(rawUrl).openConnection(); + + final int statusCode = urlConnection.getResponseCode(); + if (200 != statusCode) { + throw new IllegalStateException("Status code: " + statusCode); + } + } catch (Exception e) { + mAsserter.ok(false, "Robocop tests on your device need network/wifi access to reach: [" + rawUrl + "].", e.toString()); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + /** + * Ensure that the screen on the test device is powered on during tests. + */ + public void throwIfScreenNotOn() { + final PowerManager pm = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE); + mAsserter.ok(pm.isScreenOn(), + "Robocop tests need the test device screen to be powered on.", ""); + } + + protected GeckoProfile getTestProfile() { + if (mProfile.startsWith("/")) { + return GeckoProfile.get(getActivity(), /* profileName */ null, mProfile); + } + + return GeckoProfile.get(getActivity(), mProfile); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java new file mode 100644 index 000000000..a8dfedc4e --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java @@ -0,0 +1,976 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashSet; + +import org.json.JSONException; +import org.json.JSONObject; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Element; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.R; +import org.mozilla.gecko.RobocopUtils; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; + +import android.content.ContentValues; +import android.content.res.AssetManager; +import android.database.Cursor; +import android.os.Build; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.robotium.solo.Condition; +import com.robotium.solo.Timeout; + +/** + * A convenient base class suitable for most Robocop tests. + */ +@SuppressWarnings("unchecked") +abstract class BaseTest extends BaseRobocopTest { + private static final int VERIFY_URL_TIMEOUT = 2000; + private static final int MAX_WAIT_ENABLED_TEXT_MS = 15000; + private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000; + private static final int MAX_WAIT_VERIFY_PAGE_TITLE_MS = 15000; + public static final int MAX_WAIT_MS = 4500; + public static final int LONG_PRESS_TIME = 6000; + private static final int GECKO_READY_WAIT_MS = 180000; + + protected static final String URL_HTTP_PREFIX = "http://"; + + public Device mDevice; + protected DatabaseHelper mDatabaseHelper; + protected int mScreenMidWidth; + protected int mScreenMidHeight; + private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>(); + + protected void blockForDelayedStartup() { + try { + Actions.EventExpecter delayedStartupExpector = mActions.expectGeckoEvent("Gecko:DelayedStartup"); + delayedStartupExpector.blockForEvent(GECKO_READY_WAIT_MS, true); + delayedStartupExpector.unregisterListener(); + } catch (Exception e) { + mAsserter.dumpLog("Exception in blockForDelayedStartup", e); + } + } + + protected void blockForGeckoReady() { + try { + Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:Ready"); + if (!GeckoThread.isRunning()) { + geckoReadyExpector.blockForEvent(GECKO_READY_WAIT_MS, true); + } + geckoReadyExpector.unregisterListener(); + } catch (Exception e) { + mAsserter.dumpLog("Exception in blockForGeckoReady", e); + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + mDevice = new Device(); + mDatabaseHelper = new DatabaseHelper(getActivity(), mAsserter); + + // Ensure Robocop tests have access to network, and are run with Display powered on. + throwIfHttpGetFails(); + throwIfScreenNotOn(); + } + + protected void initializeProfile() { + final GeckoProfile profile = getTestProfile(); + + // In Robocop tests, we typically don't get initialized correctly, because + // GeckoProfile doesn't create the profile directory. + profile.enqueueInitialization(profile.getDir()); + } + + /** + * Click on the URL bar to focus it and enter editing mode. + */ + protected final void focusUrlBar() { + // Click on the browser toolbar to enter editing mode + mSolo.waitForView(R.id.browser_toolbar); + final View toolbarView = mSolo.getView(R.id.browser_toolbar); + mSolo.clickOnView(toolbarView); + + // Wait for highlighed text to gain focus + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + mSolo.waitForView(R.id.url_edit_text); + EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text); + if (urlEditText.isInputMethodTarget()) { + return true; + } + return false; + } + }, MAX_WAIT_ENABLED_TEXT_MS); + + mAsserter.ok(success, "waiting for urlbar text to gain focus", "urlbar text gained focus"); + } + + protected final void enterUrl(String url) { + focusUrlBar(); + + final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text); + + // Send the keys for the URL we want to enter + mSolo.clearEditText(urlEditView); + mSolo.typeText(urlEditView, url); + + // Get the URL text from the URL bar EditText view + final String urlBarText = urlEditView.getText().toString(); + mAsserter.is(url, urlBarText, "URL typed properly"); + } + + protected final Fragment getBrowserSearch() { + final FragmentManager fm = ((FragmentActivity) getActivity()).getSupportFragmentManager(); + return fm.findFragmentByTag("browser_search"); + } + + protected final void hitEnterAndWait() { + Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded"); + mActions.sendSpecialKey(Actions.SpecialKey.ENTER); + // wait for screen to load + contentEventExpecter.blockForEvent(); + contentEventExpecter.unregisterListener(); + } + + /** + * Load <code>url</code> by sending key strokes to the URL bar UI. + * + * This method waits synchronously for the <code>DOMContentLoaded</code> + * message from Gecko before returning. + * + * Unless you need to test text entry in the url bar, consider using loadUrl + * instead -- it loads pages more directly and quickly. + */ + protected final void inputAndLoadUrl(String url) { + enterUrl(url); + hitEnterAndWait(); + } + + /** + * Load <code>url</code> using the internal + * <code>org.mozilla.gecko.Tabs</code> API. + * + * This method does not wait for any confirmation from Gecko before + * returning -- consider using verifyUrlBarTitle or a similar approach + * to wait for the page to load, or at least use loadUrlAndWait. + */ + protected final void loadUrl(final String url) { + try { + Tabs.getInstance().loadUrl(url); + } catch (Exception e) { + mAsserter.dumpLog("Exception in loadUrl", e); + throw new RuntimeException(e); + } + } + + /** + * Load <code>url</code> using the internal + * <code>org.mozilla.gecko.Tabs</code> API and wait for DOMContentLoaded. + */ + protected final void loadUrlAndWait(final String url) { + Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded"); + loadUrl(url); + contentEventExpecter.blockForEvent(); + contentEventExpecter.unregisterListener(); + } + + protected final void closeTab(int tabId) { + Tabs tabs = Tabs.getInstance(); + Tab tab = tabs.getTab(tabId); + tabs.closeTab(tab); + } + + public final void verifyUrl(String url) { + final EditText urlEditText = (EditText) mSolo.getView(R.id.url_edit_text); + String urlBarText = null; + if (urlEditText != null) { + // wait for a short time for the expected text, in case there is a delay + // in updating the view + waitForCondition(new VerifyTextViewText(urlEditText, url), VERIFY_URL_TIMEOUT); + urlBarText = urlEditText.getText().toString(); + + } + mAsserter.is(urlBarText, url, "Browser toolbar URL stayed the same"); + } + + class VerifyTextViewText implements Condition { + private final TextView mTextView; + private final String mExpected; + public VerifyTextViewText(TextView textView, String expected) { + mTextView = textView; + mExpected = expected; + } + + @Override + public boolean isSatisfied() { + String textValue = mTextView.getText().toString(); + return mExpected.equals(textValue); + } + } + + class VerifyContentDescription implements Condition { + private final View view; + private final String expected; + + public VerifyContentDescription(View view, String expected) { + this.view = view; + this.expected = expected; + } + + @Override + public boolean isSatisfied() { + final CharSequence actual = view.getContentDescription(); + return TextUtils.equals(actual, expected); + } + } + + protected final String getAbsoluteUrl(String url) { + return mBaseHostnameUrl + "/" + url.replaceAll("(^/)", ""); + } + + protected final String getAbsoluteRawUrl(String url) { + return mBaseIpUrl + "/" + url.replaceAll("(^/)", ""); + } + + /* + * Wrapper method for mSolo.waitForCondition with additional logging. + */ + protected final boolean waitForCondition(Condition condition, int timeout) { + boolean result = mSolo.waitForCondition(condition, timeout); + if (!result) { + // Log timeout failure for diagnostic purposes only; a failed wait may + // be normal and does not necessarily warrant a test assertion/failure. + mAsserter.dumpLog("waitForCondition timeout after " + timeout + " ms."); + } + return result; + } + + public void SqliteCompare(String dbName, String sqlCommand, ContentValues[] cvs) { + File profile = new File(mProfile); + String dbPath = new File(profile, dbName).getPath(); + + Cursor c = mActions.querySql(dbPath, sqlCommand); + SqliteCompare(c, cvs); + } + + public void SqliteCompare(Cursor c, ContentValues[] cvs) { + mAsserter.is(c.getCount(), cvs.length, "List is correct length"); + if (c.moveToFirst()) { + do { + boolean found = false; + for (int i = 0; !found && i < cvs.length; i++) { + if (CursorMatches(c, cvs[i])) { + found = true; + } + } + mAsserter.is(found, true, "Password was found"); + } while (c.moveToNext()); + } + } + + public boolean CursorMatches(Cursor c, ContentValues cv) { + for (int i = 0; i < c.getColumnCount(); i++) { + String column = c.getColumnName(i); + if (cv.containsKey(column)) { + mAsserter.info("Comparing", "Column values for: " + column); + Object value = cv.get(column); + if (value == null) { + if (!c.isNull(i)) { + return false; + } + } else { + if (c.isNull(i) || !value.toString().equals(c.getString(i))) { + return false; + } + } + } + } + return true; + } + + public InputStream getAsset(String filename) throws IOException { + AssetManager assets = getInstrumentation().getContext().getAssets(); + return assets.open(filename); + } + + public boolean waitForText(final String text) { + // false is the default value for finding only + // visible views in `Solo.waitForText(String)`. + return waitForText(text, false); + } + + public boolean waitForText(final String text, final boolean onlyVisibleViews) { + // We use the default robotium values from + // `Waiter.waitForText(String)` for unspecified arguments. + final boolean rc = + mSolo.waitForText(text, 0, Timeout.getLargeTimeout(), true, onlyVisibleViews); + if (!rc) { + // log out failed wait for diagnostic purposes only; + // waitForText failures are sometimes expected/normal + mAsserter.dumpLog("waitForText timeout on "+text); + } + return rc; + } + + // waitForText usually scrolls down in a view when text is not visible. + // For PreferenceScreens and dialogs, Solo.waitForText scrolling does not + // work, so we use this hack to do the same thing. + protected boolean waitForPreferencesText(String txt) { + boolean foundText = waitForText(txt); + if (!foundText) { + if ((mScreenMidWidth == 0) || (mScreenMidHeight == 0)) { + mScreenMidWidth = mDriver.getGeckoWidth()/2; + mScreenMidHeight = mDriver.getGeckoHeight()/2; + } + + // If we don't see the item, scroll down once in case it's off-screen. + // Hacky way to scroll down. solo.scroll* does not work in dialogs. + MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop()); + meh.dragSync(mScreenMidWidth, mScreenMidHeight+100, mScreenMidWidth, mScreenMidHeight-100); + + foundText = mSolo.waitForText(txt); + } + return foundText; + } + + /** + * Wait for <text> to be visible and also be enabled/clickable. + */ + public boolean waitForEnabledText(String text) { + final String testText = text; + boolean rc = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + // Solo.getText() could be used here, except that it sometimes + // hits an assertion when the requested text is not found. + ArrayList<View> views = mSolo.getCurrentViews(); + for (View view : views) { + if (view instanceof TextView) { + TextView tv = (TextView)view; + String viewText = tv.getText().toString(); + if (tv.isEnabled() && viewText != null && viewText.matches(testText)) { + return true; + } + } + } + return false; + } + }, MAX_WAIT_ENABLED_TEXT_MS); + if (!rc) { + // log out failed wait for diagnostic purposes only; + // failures are sometimes expected/normal + mAsserter.dumpLog("waitForEnabledText timeout on "+text); + } + return rc; + } + + + /** + * Select <item> from Menu > "Settings" > <section>. + */ + public void selectSettingsItem(String section, String item) { + String[] itemPath = { "Settings", section, item }; + selectMenuItemByPath(itemPath); + } + + /** + * Traverses the items in listItems in order in the menu. + */ + public void selectMenuItemByPath(String[] listItems) { + int listLength = listItems.length; + if (listLength > 0) { + selectMenuItem(listItems[0]); + } + if (listLength > 1) { + for (int i = 1; i < listLength; i++) { + String itemName = "^" + listItems[i] + "$"; + mAsserter.ok(waitForPreferencesText(itemName), "Waiting for and scrolling once to find item " + itemName, itemName + " found"); + mAsserter.ok(waitForEnabledText(itemName), "Waiting for enabled text " + itemName, itemName + " option is present and enabled"); + mSolo.clickOnText(itemName); + } + } + } + + public final void selectMenuItem(String menuItemName) { + // build the item name ready to be used + String itemName = "^" + menuItemName + "$"; + final View menuView = mSolo.getView(R.id.menu); + mAsserter.isnot(menuView, null, "Menu view is not null"); + mSolo.clickOnView(menuView, true); + mAsserter.ok(waitForEnabledText(itemName), "Waiting for menu item " + itemName, itemName + " is present and enabled"); + mSolo.clickOnText(itemName); + } + + public final void verifyHomePagerHidden() { + final View homePagerContainer = mSolo.getView(R.id.home_screen_container); + + boolean rc = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return homePagerContainer.getVisibility() != View.VISIBLE; + } + }, MAX_WAIT_HOME_PAGER_HIDDEN_MS); + + if (!rc) { + mAsserter.ok(rc, "Verify HomePager is hidden", "HomePager is hidden"); + } + } + + public final void verifyUrlBarTitle(String url) { + mAsserter.isnot(url, null, "The url argument is not null"); + + final String expected; + if (mStringHelper.ABOUT_HOME_URL.equals(url)) { + expected = mStringHelper.ABOUT_HOME_TITLE; + } else if (url.startsWith(URL_HTTP_PREFIX)) { + expected = url.substring(URL_HTTP_PREFIX.length()); + } else { + expected = url; + } + + final TextView urlBarTitle = (TextView) mSolo.getView(R.id.url_bar_title); + String pageTitle = null; + if (urlBarTitle != null) { + // Wait for the title to make sure it has been displayed in case the view + // does not update fast enough + waitForCondition(new VerifyTextViewText(urlBarTitle, expected), MAX_WAIT_VERIFY_PAGE_TITLE_MS); + pageTitle = urlBarTitle.getText().toString(); + } + mAsserter.is(pageTitle, expected, "Page title is correct"); + } + + public final void verifyUrlInContentDescription(String url) { + mAsserter.isnot(url, null, "The url argument is not null"); + + final String expected; + if (mStringHelper.ABOUT_HOME_URL.equals(url)) { + expected = mStringHelper.ABOUT_HOME_TITLE; + } else if (url.startsWith(URL_HTTP_PREFIX)) { + expected = url.substring(URL_HTTP_PREFIX.length()); + } else { + expected = url; + } + + final View urlDisplayLayout = mSolo.getView(R.id.display_layout); + assertNotNull("ToolbarDisplayLayout is not null", urlDisplayLayout); + + String actualUrl = null; + + // Wait for the title to make sure it has been displayed in case the view + // does not update fast enough + waitForCondition(new VerifyContentDescription(urlDisplayLayout, expected), MAX_WAIT_VERIFY_PAGE_TITLE_MS); + if (urlDisplayLayout.getContentDescription() != null) { + actualUrl = urlDisplayLayout.getContentDescription().toString(); + } + + mAsserter.is(actualUrl, expected, "Url is correct"); + } + + public final void verifyTabCount(int expectedTabCount) { + Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter); + String tabCountText = tabCount.getText(); + int tabCountInt = Integer.parseInt(tabCountText); + mAsserter.is(tabCountInt, expectedTabCount, "The correct number of tabs are opened"); + } + + public void verifyPinned(final boolean isPinned, final String gridItemTitle) { + boolean viewFound = waitForText(gridItemTitle); + mAsserter.ok(viewFound, "Found top site title: " + gridItemTitle, null); + + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + // We set the left compound drawable (index 0) to the pin icon. + final TextView gridItemTextView = mSolo.getText(gridItemTitle); + return isPinned == (gridItemTextView.getCompoundDrawables()[0] != null); + } + }, MAX_WAIT_MS); + mAsserter.ok(success, "Top site item was pinned: " + isPinned, null); + } + + public void pinTopSite(String gridItemTitle) { + verifyPinned(false, gridItemTitle); + mSolo.clickLongOnText(gridItemTitle); + boolean dialogOpened = mSolo.waitForDialogToOpen(); + mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null); + boolean pinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_PIN_SITE); + mAsserter.ok(pinSiteFound, "Found pin site menu item", null); + mSolo.clickOnText(mStringHelper.CONTEXT_MENU_PIN_SITE); + verifyPinned(true, gridItemTitle); + } + + public void unpinTopSite(String gridItemTitle) { + verifyPinned(true, gridItemTitle); + mSolo.clickLongOnText(gridItemTitle); + boolean dialogOpened = mSolo.waitForDialogToOpen(); + mAsserter.ok(dialogOpened, "Pin site dialog opened: " + gridItemTitle, null); + boolean unpinSiteFound = waitForText(mStringHelper.CONTEXT_MENU_UNPIN_SITE); + mAsserter.ok(unpinSiteFound, "Found unpin site menu item", null); + mSolo.clickOnText(mStringHelper.CONTEXT_MENU_UNPIN_SITE); + verifyPinned(false, gridItemTitle); + } + + // Used to perform clicks on pop-up buttons without having to close the virtual keyboard + public void clickOnButton(String label) { + final Button button = mSolo.getButton(label); + try { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + button.performClick(); + } + }); + } catch (Throwable throwable) { + mAsserter.ok(false, "Unable to click the button","Was unable to click button "); + } + } + + private void waitForAnimationsToFinish() { + // Ideally we'd actually wait for animations to finish but since we have + // no good way of doing that, we just wait an arbitrary unit of time. + mSolo.sleep(3500); + } + + public void addTab() { + mSolo.clickOnView(mSolo.getView(R.id.tabs)); + waitForAnimationsToFinish(); + + // wait for addTab to appear (this is usually immediate) + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + View addTabView = mSolo.getView(R.id.add_tab); + if (addTabView == null) { + return false; + } + return true; + } + }, MAX_WAIT_MS); + mAsserter.ok(success, "waiting for add tab view", "add tab view available"); + final Actions.RepeatedEventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); + mSolo.clickOnView(mSolo.getView(R.id.add_tab)); + waitForAnimationsToFinish(); + + // Wait until we get a PageShow event for a new tab ID + for(;;) { + try { + JSONObject data = new JSONObject(pageShowExpecter.blockForEventData()); + int tabID = data.getInt("tabID"); + if (tabID == 0) { + mAsserter.dumpLog("addTab ignoring PageShow for tab 0"); + continue; + } + if (!mKnownTabIDs.contains(tabID)) { + mKnownTabIDs.add(tabID); + break; + } + } catch(JSONException e) { + mAsserter.ok(false, "Exception in addTab", getStackTraceString(e)); + } + } + pageShowExpecter.unregisterListener(); + } + + public void addTab(String url) { + addTab(); + + // Adding a new tab opens about:home, so now we just need to load the url in it. + loadUrlAndWait(url); + } + + public void closeAddedTabs() { + for(int tabID : mKnownTabIDs) { + closeTab(tabID); + } + } + + // A temporary tabs list/grid holder while the list and grid views are being transitioned to + // RecyclerViews (bug 1116415 and bug 1310081). + private static class TabsView { + private AdapterView<ListAdapter> gridView; + private RecyclerView listView; + + public TabsView(View view) { + if (view instanceof RecyclerView) { + listView = (RecyclerView) view; + } else { + gridView = (AdapterView<ListAdapter>) view; + } + } + + public void bringPositionIntoView(int index) { + if (gridView != null) { + gridView.setSelection(index); + } else { + listView.scrollToPosition(index); + } + } + + public View getViewAtIndex(int index) { + if (gridView != null) { + return gridView.getChildAt(index - gridView.getFirstVisiblePosition()); + } else { + final RecyclerView.ViewHolder itemViewHolder = listView.findViewHolderForLayoutPosition(index); + return itemViewHolder == null ? null : itemViewHolder.itemView; + } + } + + public void post(Runnable runnable) { + if (gridView != null) { + gridView.post(runnable); + } else { + listView.post(runnable); + } + } + } + /** + * Gets the AdapterView of the tabs list. + * + * @return List view in the tabs panel + */ + private final TabsView getTabsLayout() { + Element tabs = mDriver.findElement(getActivity(), R.id.tabs); + tabs.click(); + return new TabsView(getActivity().findViewById(R.id.normal_tabs)); + } + + /** + * Gets the view in the tabs panel at the specified index. + * + * @return View at index + */ + private View getTabViewAt(final int index) { + final View[] childView = { null }; + + final TabsView view = getTabsLayout(); + + runOnUiThreadSync(new Runnable() { + @Override + public void run() { + view.bringPositionIntoView(index); + + // The selection isn't updated synchronously; posting a + // runnable to the view's queue guarantees we'll run after the + // layout pass. + view.post(new Runnable() { + @Override + public void run() { + // Index is relative to all views in the list. + childView[0] = view.getViewAtIndex(index); + } + }); + } + }); + + boolean result = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return childView[0] != null; + } + }, MAX_WAIT_MS); + + mAsserter.ok(result, "list item at index " + index + " exists", null); + + return childView[0]; + } + + /** + * Selects the tab at the specified index. + * + * @param index Index of tab to select + */ + public void selectTabAt(final int index) { + mSolo.clickOnView(getTabViewAt(index)); + } + + public final void runOnUiThreadSync(Runnable runnable) { + RobocopUtils.runOnUiThreadSync(getActivity(), runnable); + } + + /* Tap the "star" (bookmark) button to bookmark or un-bookmark the current page */ + public void toggleBookmark() { + mActions.sendSpecialKey(Actions.SpecialKey.MENU); + waitForText("Settings"); + + // On ICS+ phones, there is no button labeled "Bookmarks" + // instead we have to just dig through every button on the screen + ArrayList<View> images = mSolo.getCurrentViews(); + for (int i = 0; i < images.size(); i++) { + final View view = images.get(i); + boolean found = false; + found = "Bookmark".equals(view.getContentDescription()); + + // on older android versions, try looking at the button's text + if (!found) { + if (view instanceof TextView) { + found = "Bookmark".equals(((TextView)view).getText()); + } + } + + if (found) { + int[] xy = new int[2]; + view.getLocationOnScreen(xy); + + final int viewWidth = view.getWidth(); + final int viewHeight = view.getHeight(); + final float x = xy[0] + (viewWidth / 2.0f); + float y = xy[1] + (viewHeight / 2.0f); + + mSolo.clickOnScreen(x, y); + } + } + } + + class Device { + public final String version; // 2.x or 3.x or 4.x + public String type; // "tablet" or "phone" + public final int width; + public final int height; + public final float density; + + public Device() { + // Determine device version + int sdk = Build.VERSION.SDK_INT; + if (sdk < Build.VERSION_CODES.HONEYCOMB) { + version = "2.x"; + } else { + if (sdk > Build.VERSION_CODES.HONEYCOMB_MR2) { + version = "4.x"; + } else { + version = "3.x"; + } + } + // Determine with and height + DisplayMetrics dm = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm); + height = dm.heightPixels; + width = dm.widthPixels; + density = dm.density; + // Determine device type + type = "phone"; + try { + if (GeckoAppShell.isTablet()) { + type = "tablet"; + } + } catch (Exception e) { + mAsserter.dumpLog("Exception in detectDevice", e); + } + } + } + + class Navigation { + private final String devType; + private final String osVersion; + + public Navigation(Device mDevice) { + devType = mDevice.type; + osVersion = mDevice.version; + } + + public void back() { + Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); + + if (devType.equals("tablet")) { + Element backBtn = mDriver.findElement(getActivity(), R.id.back); + backBtn.click(); + } else { + mSolo.goBack(); + } + + pageShowExpecter.blockForEvent(); + pageShowExpecter.unregisterListener(); + } + + public void forward() { + Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); + + if (devType.equals("tablet")) { + mSolo.waitForView(R.id.forward); + mSolo.clickOnView(mSolo.getView(R.id.forward)); + } else { + mActions.sendSpecialKey(Actions.SpecialKey.MENU); + waitForText("^New Tab$"); + if (!osVersion.equals("2.x")) { + mSolo.waitForView(R.id.forward); + mSolo.clickOnView(mSolo.getView(R.id.forward)); + } else { + mSolo.clickOnText("^Forward$"); + } + ensureMenuClosed(); + } + + pageShowExpecter.blockForEvent(); + pageShowExpecter.unregisterListener(); + } + + // DEPRECATED! + // Use BaseTest.toggleBookmark() in new code. + public void bookmark() { + mActions.sendSpecialKey(Actions.SpecialKey.MENU); + waitForText("^New Tab$"); + if (mSolo.searchText("^Bookmark$")) { + // This is the Android 2.x so the button has text + mSolo.clickOnText("^Bookmark$"); + } else { + Element bookmarkBtn = mDriver.findElement(getActivity(), R.id.bookmark); + if (bookmarkBtn != null) { + // We are on Android 4.x so the button is an image button + bookmarkBtn.click(); + } + } + ensureMenuClosed(); + } + + // On some devices, the menu may not be dismissed after clicking on an + // item. Close it here. + private void ensureMenuClosed() { + if (mSolo.searchText("^New Tab$")) { + mSolo.goBack(); + } + } + } + + /** + * Gets the string representation of a stack trace. + * + * @param t Throwable to get stack trace for + * @return Stack trace as a string + */ + public static String getStackTraceString(Throwable t) { + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + /** + * Condition class that waits for a view, and allows callers access it when done. + */ + private class DescriptionCondition<T extends View> implements Condition { + public T mView; + private final String mDescr; + private final Class<T> mCls; + + public DescriptionCondition(Class<T> cls, String descr) { + mDescr = descr; + mCls = cls; + } + + @Override + public boolean isSatisfied() { + mView = findViewWithContentDescription(mCls, mDescr); + return (mView != null); + } + } + + /** + * Wait for a view with the specified description . + */ + public <T extends View> T waitForViewWithDescription(Class<T> cls, String description) { + DescriptionCondition<T> c = new DescriptionCondition<T>(cls, description); + waitForCondition(c, MAX_WAIT_ENABLED_TEXT_MS); + return c.mView; + } + + /** + * Get an active view with the specified description . + */ + public <T extends View> T findViewWithContentDescription(Class<T> cls, String description) { + for (T view : mSolo.getCurrentViews(cls)) { + final String descr = (String) view.getContentDescription(); + if (TextUtils.isEmpty(descr)) { + continue; + } + + if (TextUtils.equals(description, descr)) { + return view; + } + } + + return null; + } + + /** + * Abstract class for running small test cases within a BaseTest. + */ + abstract class TestCase implements Runnable { + /** + * Implement tests here. setUp and tearDown for the test case + * should be handled by the parent test. This is so we can avoid the + * overhead of starting Gecko and creating profiles. + */ + protected abstract void test() throws Exception; + + @Override + public void run() { + try { + test(); + } catch (Exception e) { + mAsserter.ok(false, + "Test " + this.getClass().getName() + " threw exception: " + e, + ""); + } + } + } + + /** + * Set the preference and wait for it to change before proceeding with the test. + */ + public void setPreferenceAndWaitForChange(final String name, final Object value) { + blockForGeckoReady(); + mActions.setPref(name, value, /* flush */ false); + + // Wait for confirmation of the pref change before proceeding with the test. + mActions.getPrefs(new String[] { name }, new Actions.PrefHandlerBase() { + + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, boolean changedValue) { + mAsserter.is(pref, name, "Expecting correct pref name"); + mAsserter.ok(value instanceof Boolean, "Expecting boolean pref", ""); + mAsserter.is(changedValue, value, "Expecting matching pref value"); + } + + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, int changedValue) { + mAsserter.is(pref, name, "Expecting correct pref name"); + mAsserter.ok(value instanceof Integer, "Expecting int pref", ""); + mAsserter.is(changedValue, value, "Expecting matching pref value"); + } + + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, String changedValue) { + mAsserter.is(pref, name, "Expecting correct pref name"); + mAsserter.ok(value instanceof CharSequence, "Expecting string pref", ""); + mAsserter.is(changedValue, value, "Expecting matching pref value"); + } + + }).waitForFinish(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java new file mode 100644 index 000000000..5a1d09f8c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.util.Clipboard; +import org.mozilla.gecko.Element; +import org.mozilla.gecko.R; + +import android.util.DisplayMetrics; + +import com.robotium.solo.Condition; + +/** + * This class covers interactions with the context menu opened from web content + */ +abstract class ContentContextMenuTest extends PixelTest { + private static final int MAX_TEST_TIMEOUT = 30000; // 30 seconds (worst case) + + // This method opens the context menu of any web content. It assumes that the page is already loaded + protected void openWebContentContextMenu(String waitText) { + DisplayMetrics dm = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm); + + // The web content we are trying to open the context menu for should be positioned at the top of the page, at least 60px high and aligned to the middle + float top = mDriver.getGeckoTop() + 30 * dm.density; + float left = mDriver.getGeckoLeft() + mDriver.getGeckoWidth() / 2; + + mAsserter.dumpLog("long-clicking at "+left+", "+top); + mSolo.clickLongOnScreen(left, top); + waitForText(waitText); + } + + protected void verifyContextMenuItems(String[] items) { + // Test that the menu items are displayed + if (!mSolo.searchText(items[0])) { + openWebContentContextMenu(items[0]); // Open the context menu if it is not already + } + + for (String option:items) { + mAsserter.ok(mSolo.searchText(option), "Checking that the option: " + option + " is available", "The option is available"); + } + } + + protected void openTabFromContextMenu(String contextMenuOption, int expectedTabCount) { + if (!mSolo.searchText(contextMenuOption)) { + openWebContentContextMenu(contextMenuOption); // Open the context menu if it is not already + } + Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added"); + mSolo.clickOnText(contextMenuOption); + tabEventExpecter.blockForEvent(); + tabEventExpecter.unregisterListener(); + verifyTabCount(expectedTabCount); + } + + protected void verifyTabs(String[] items) { + if (!mSolo.searchText(items[0])) { + openWebContentContextMenu(items[0]); + } + + for (String option:items) { + mAsserter.ok(mSolo.searchText(option), "Checking that the option: " + option + " is available", "The option is available"); + } + } + + protected void switchTabs(String tab) { + if (!mSolo.searchText(tab)) { + openWebContentContextMenu(tab); + } + mSolo.clickOnText(tab); + } + + + protected void verifyCopyOption(String copyOption, final String copiedText) { + if (!mSolo.searchText(copyOption)) { + openWebContentContextMenu(copyOption); // Open the context menu if it is not already + } + mSolo.clickOnText(copyOption); + boolean correctText = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + final String clipboardText = Clipboard.getText(); + mAsserter.dumpLog("Clipboard text = " + clipboardText + " , expected text = " + copiedText); + return clipboardText.contains(copiedText); + } + }, MAX_TEST_TIMEOUT); + mAsserter.ok(correctText, "Checking if the text is correctly copied", "The text was correctly copied"); + } + + + + protected void verifyShareOption(String shareOption, String pageTitle) { + waitForText(pageTitle); // Even if this fails, it won't assert + if (!mSolo.searchText(shareOption)) { + openWebContentContextMenu(shareOption); // Open the context menu if it is not already + } + mSolo.clickOnText(shareOption); + mAsserter.ok(waitForText(shareOption), "Checking that the share pop-up is displayed", "The pop-up has been displayed"); + + // Close the Share Link option menu and wait for the page to be focused again + mSolo.goBack(); + waitForText(pageTitle); + } + + protected void verifyViewImageOption(String viewImageOption, final String imageUrl, String pageTitle) { + if (!mSolo.searchText(viewImageOption)) { + openWebContentContextMenu(viewImageOption); + } + mSolo.clickOnText(viewImageOption); + + boolean viewedImage = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + final Element urlBarElement = mDriver.findElement(getActivity(), R.id.url_edit_text); + final String loadedUrl = urlBarElement.getText(); + return loadedUrl.contentEquals(imageUrl); + } + }, MAX_TEST_TIMEOUT); + mAsserter.ok(viewedImage, "Checking if the image is correctly viewed", "The image was correctly viewed"); + + mSolo.goBack(); + waitForText(pageTitle); + } + + protected void verifyBookmarkLinkOption(String bookmarkOption, String link) { + if (!mSolo.searchText(bookmarkOption)) { + openWebContentContextMenu(bookmarkOption); // Open the context menu if it is not already + } + mSolo.clickOnText(bookmarkOption); + mAsserter.ok(waitForText("Bookmark added"), "Waiting for the Bookmark added toaster notification", "The notification has been displayed"); + mAsserter.ok(mDatabaseHelper.isBookmark(link), "Checking if the link has been added as a bookmark", "The link has been bookmarked"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java new file mode 100644 index 000000000..5496c97d2 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentProviderTest.java @@ -0,0 +1,255 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.concurrent.Callable; + +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserProvider; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.test.IsolatedContext; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentResolver; +import android.test.mock.MockContext; + +/* + * ContentProviderTest provides the infrastructure to run content provider + * tests in an controlled/isolated environment, guaranteeing that your tests + * will not affect or be affected by any UI-related code. This is basically + * a heavily adapted port of Android's ProviderTestCase2 to work on Mozilla's + * infrastructure. + * + * For some tests, we need to have access to UI parts, or at least launch + * the activity so the assets with test data become available, which requires + * that we derive this test from BaseTest and consequently pull in some more + * UI code than we'd ideally want. Furthermore, we need to pass the + * Activity and not the instrumentation Context for the UI part to find some + * of its required resources. + * Similarly, we need to pass the Activity instead of the Instrumentation + * Context down to some of our users (still wrapped in the Delegating Provider) + * because they will stop working correctly if we do not. A typical problem + * is that databases used in the ContentProvider will be attempted to be + * opened twice. + */ +abstract class ContentProviderTest extends BaseTest { + protected ContentProvider mProvider; + protected ChangeRecordingMockContentResolver mResolver; + protected ArrayList<Runnable> mTests; + protected String mDatabaseName; + protected String mProviderAuthority; + protected IsolatedContext mProviderContext; + + private class ContentProviderMockContext extends MockContext { + @Override + public Resources getResources() { + // We will fail to find some resources if we don't point + // at the original activity. + return ((Context)getActivity()).getResources(); + } + + @Override + public String getPackageName() { + return getInstrumentation().getContext().getPackageName(); + } + + @Override + public String getPackageResourcePath() { + return getInstrumentation().getContext().getPackageResourcePath(); + } + + @Override + public File getDir(String name, int mode) { + return getInstrumentation().getContext().getDir(this.getClass().getSimpleName() + "_" + name, mode); + } + + @Override + public Context getApplicationContext() { + return this; + } + + @Override + public SharedPreferences getSharedPreferences(String name, int mode) { + return getInstrumentation().getContext().getSharedPreferences(name, mode); + } + + @Override + public ApplicationInfo getApplicationInfo() { + return getInstrumentation().getContext().getApplicationInfo(); + } + } + + protected class DelegatingTestContentProvider extends ContentProvider { + ContentProvider mTargetProvider; + + public DelegatingTestContentProvider(ContentProvider targetProvider) { + super(); + mTargetProvider = targetProvider; + } + + private Uri appendTestParam(Uri uri) { + try { + return appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1"); + } catch (Exception e) {} + + return null; + } + + @Override + public boolean onCreate() { + return mTargetProvider.onCreate(); + } + + @Override + public String getType(Uri uri) { + return mTargetProvider.getType(uri); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return mTargetProvider.delete(appendTestParam(uri), selection, selectionArgs); + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return mTargetProvider.insert(appendTestParam(uri), values); + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + return mTargetProvider.update(appendTestParam(uri), values, + selection, selectionArgs); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + return mTargetProvider.query(appendTestParam(uri), projection, selection, + selectionArgs, sortOrder); + } + + @Override + public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + return mTargetProvider.applyBatch(operations); + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + return mTargetProvider.bulkInsert(appendTestParam(uri), values); + } + + public ContentProvider getTargetProvider() { + return mTargetProvider; + } + } + + /* + * A MockContentResolver that records each URI that is supplied to + * notifyChange. Warning: the list of changed URIs is not + * synchronized. + */ + protected class ChangeRecordingMockContentResolver extends MockContentResolver { + public final LinkedList<Uri> notifyChangeList = new LinkedList<Uri>(); + + @Override + public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { + notifyChangeList.addLast(uri); + + super.notifyChange(uri, observer, syncToNetwork); + } + } + + /** + * Factory function that makes new ContentProvider instances. + * <p> + * We want a fresh provider each test, so this should be invoked in + * <code>setUp</code> before each individual test. + */ + protected static Callable<ContentProvider> sBrowserProviderCallable = new Callable<ContentProvider>() { + @Override + public ContentProvider call() { + return new BrowserProvider(); + } + }; + + private void setUpContentProvider(ContentProvider targetProvider) throws Exception { + mResolver = new ChangeRecordingMockContentResolver(); + + final String filenamePrefix = this.getClass().getSimpleName() + "."; + RenamingDelegatingContext targetContextWrapper = + new RenamingDelegatingContext( + new ContentProviderMockContext(), + (Context)getActivity(), + filenamePrefix); + + mProviderContext = new IsolatedContext(mResolver, targetContextWrapper); + + targetProvider.attachInfo(mProviderContext, null); + + mProvider = new DelegatingTestContentProvider(targetProvider); + mProvider.attachInfo(mProviderContext, null); + + mResolver.addProvider(mProviderAuthority, mProvider); + } + + public static Uri appendUriParam(Uri uri, String param, String value) { + return uri.buildUpon().appendQueryParameter(param, value).build(); + } + + public void setTestName(String testName) { + mAsserter.setTestName(this.getClass().getName() + " - " + testName); + } + + @Override + public void setUp() throws Exception { + throw new UnsupportedOperationException("You should call setUp(authority, databaseName) instead"); + } + + public void setUp(Callable<ContentProvider> contentProviderFactory, String authority, String databaseName) throws Exception { + super.setUp(); + + mTests = new ArrayList<Runnable>(); + mDatabaseName = databaseName; + + mProviderAuthority = authority; + + setUpContentProvider(contentProviderFactory.call()); + } + + @Override + public void tearDown() throws Exception { + if (Build.VERSION.SDK_INT >= 11) { + mProvider.shutdown(); + } + + if (mDatabaseName != null) { + mProviderContext.deleteDatabase(mDatabaseName); + } + + super.tearDown(); + } + + public AssetManager getAssetManager() { + return getInstrumentation().getContext().getAssets(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java new file mode 100644 index 000000000..c87dc2432 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/DatabaseHelper.java @@ -0,0 +1,170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.ArrayList; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.Assert; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.db.BrowserDB; + +import android.app.Activity; +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +class DatabaseHelper { + protected enum BrowserDataType {BOOKMARKS, HISTORY}; + private final Activity mActivity; + private final Assert mAsserter; + + public DatabaseHelper(Activity activity, Assert asserter) { + mActivity = activity; + mAsserter = asserter; + } + /** + * This method can be used to check if an URL is present in the bookmarks database + */ + protected boolean isBookmark(String url) { + final ContentResolver resolver = mActivity.getContentResolver(); + return getProfileDB().isBookmark(resolver, url); + } + + protected Uri buildUri(BrowserDataType dataType) { + Uri uri = null; + if (dataType == BrowserDataType.BOOKMARKS || dataType == BrowserDataType.HISTORY) { + uri = Uri.parse("content://" + AppConstants.ANDROID_PACKAGE_NAME + ".db.browser/" + dataType.toString().toLowerCase()); + } else { + mAsserter.ok(false, "The wrong data type has been provided = " + dataType.toString(), "Please provide the correct data type"); + } + uri = uri.buildUpon().appendQueryParameter("profile", GeckoProfile.DEFAULT_PROFILE) + .appendQueryParameter("sync", "true").build(); + return uri; + } + + /** + * Adds a bookmark. + */ + protected void addMobileBookmark(String title, String url) { + final ContentResolver resolver = mActivity.getContentResolver(); + getProfileDB().addBookmark(resolver, title, url); + mAsserter.ok(true, "Inserting a new bookmark", "Inserting the bookmark with the title = " + title + " and the url = " + url); + } + + /** + * Updates the title and keyword of a bookmark with the given URL. + * + * Warning: This method assumes that there's only one bookmark with the given URL. + */ + protected void updateBookmark(String url, String title, String keyword) { + final ContentResolver resolver = mActivity.getContentResolver(); + // Get the id for the bookmark with the given URL. + Cursor c = null; + try { + c = getProfileDB().getBookmarkForUrl(resolver, url); + if (!c.moveToFirst()) { + mAsserter.ok(false, "Getting bookmark with url", "Couldn't find bookmark with url = " + url); + return; + } + + int id = c.getInt(c.getColumnIndexOrThrow("_id")); + getProfileDB().updateBookmark(resolver, id, url, title, keyword); + + mAsserter.ok(true, "Updating bookmark", "Updating bookmark with url = " + url); + } finally { + if (c != null) { + c.close(); + } + } + } + + protected void deleteBookmark(String url) { + final ContentResolver resolver = mActivity.getContentResolver(); + getProfileDB().removeBookmarksWithURL(resolver, url); + } + + protected void deleteHistoryItem(String url) { + final ContentResolver resolver = mActivity.getContentResolver(); + getProfileDB().removeHistoryEntry(resolver, url); + } + + // About the same implementation as getFolderIdFromGuid from LocalBrowserDB because it is declared private and we can't use reflections to access it + protected long getFolderIdFromGuid(String guid) { + final ContentResolver resolver = mActivity.getContentResolver(); + long folderId = -1L; + final Uri bookmarksUri = buildUri(BrowserDataType.BOOKMARKS); + + Cursor c = null; + try { + c = resolver.query(bookmarksUri, + new String[] { "_id" }, + "guid = ?", + new String[] { guid }, + null); + if (c.moveToFirst()) { + folderId = c.getLong(c.getColumnIndexOrThrow("_id")); + } + + if (folderId == -1) { + mAsserter.ok(false, "Trying to get the folder id" ,"We did not get the correct folder id"); + } + } finally { + if (c != null) { + c.close(); + } + } + return folderId; + } + + /** + * Returns all of the bookmarks or history entries in a database. + * + * @return an ArrayList of the urls in the Firefox for Android Bookmarks or History databases. + */ + protected ArrayList<String> getBrowserDBUrls(BrowserDataType dataType) { + final ArrayList<String> browserData = new ArrayList<String>(); + final ContentResolver resolver = mActivity.getContentResolver(); + + Cursor cursor = null; + final BrowserDB db = getProfileDB(); + if (dataType == BrowserDataType.HISTORY) { + cursor = db.getAllVisitedHistory(resolver); + } else if (dataType == BrowserDataType.BOOKMARKS) { + cursor = db.getBookmarksInFolder(resolver, getFolderIdFromGuid("mobile")); + } + + if (cursor == null) { + mAsserter.ok(false, "We could not retrieve any data from the database", "The cursor was null"); + return browserData; + } + + try { + if (!cursor.moveToFirst()) { + // Nothing here, but that's OK -- maybe there are zero results. The calling test will fail. + return browserData; + } + + do { + // The URL field may be null for folders in the structure of the Bookmarks table for Firefox. Eliminate those. + if (cursor.getString(cursor.getColumnIndex("url")) != null) { + browserData.add(cursor.getString(cursor.getColumnIndex("url"))); + } + } while (cursor.moveToNext()); + + return browserData; + } finally { + cursor.close(); + } + } + + protected BrowserDB getProfileDB() { + return BrowserDB.from(mActivity); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java new file mode 100644 index 000000000..a71f8fd49 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptBridgeTest.java @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; + +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.JavascriptBridge; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +/** + * Extended to write tests using JavascriptBridge, which allows Java and JS to communicate back-and-forth. + * If you don't need back-and-forth communication, consider {@link JavascriptTest}. + * + * To write a test: + * * Extend this class + * * Add your javascript file to the base robocop directory (see where `testJavascriptBridge.js` is located) + * * In the main test method, call {@link #blockForReadyAndLoadJS(String)} with your javascript file name + * (don't include the path) or if you're loading a non-harness url, be sure to call {@link GeckoHelper#blockForReady()} + * * You can access js calls via the {@link #getJS()} method + * - Read {@link JavascriptBridge} javadoc for more information about using the API. + */ +public class JavascriptBridgeTest extends UITest { + + private static final long WAIT_GET_FROM_JS_MILLIS = 20000; + + private JavascriptBridge js; + + // Feel free to implement additional return types. + private boolean isAsyncValueSet; + private String asyncValueStr; + + @Override + public void setUp() throws Exception { + super.setUp(); + js = new JavascriptBridge(this); + } + + @Override + public void tearDown() throws Exception { + js.disconnect(); + super.tearDown(); + } + + public JavascriptBridge getJS() { + return js; + } + + protected void blockForReadyAndLoadJS(final String jsFilename) { + NavigationHelper.enterAndLoadUrl(mStringHelper.getHarnessUrlForJavascript(jsFilename)); + } + + /** + * Used to retrieve values from js when it's required to call async methods (e.g. promises). + * This method will block until the value is retrieved else timeout. + * + * This method is not thread-safe. + * + * Ideally, we could just have Javascript call Java when the callback completes but Java won't + * listen for messages unless we call into JS again (bug 1253467). + * + * To use this method: + * * Call this method with a name argument, henceforth known as `varName`. Note that it will be capitalized + * in all function names. + * * Create a js function, `"getAsync" + varName` (e.g. if `varName == "clientId`, the function is + * `getAsyncClientId`) of no args. This function should call the async get method and assign a global variable to + * the return value. + * * Create a js function, `"pollGetAsync" + varName` (e.g. `pollGetAsyncClientId`) of no args. It should call + * `java.asyncCall('blockingFromJsResponseString', ...` with two args: a boolean if the async value has been set yet + * and a String with the global return value (`null` or `undefined` are acceptable if the value has not been set). + */ + public String getBlockingFromJsString(final String varName) { + isAsyncValueSet = false; + final String fnSuffix = capitalize(varName); + getJS().syncCall("getAsync" + fnSuffix); // Initiate async callback + + final long timeoutMillis = System.currentTimeMillis() + WAIT_GET_FROM_JS_MILLIS; + do { + // Avoid sleeping! The async callback may have already completed so + // we test for completion here, rather than in the loop predicate. + getJS().syncCall("pollGetAsync" + fnSuffix); + if (isAsyncValueSet) { + break; + } + + if (System.currentTimeMillis() > timeoutMillis) { + fFail("Retrieving " + varName + " from JS has timed out"); + } + try { + Thread.sleep(500, 0); // Give time for JS to complete its operation. (emulator one core?) + } catch (final InterruptedException e) { } + } while (true); + + return asyncValueStr; + } + + public void blockingFromJsResponseString(final boolean isValueSet, final String value) { + this.isAsyncValueSet = isValueSet; + this.asyncValueStr = value; + } + + private String capitalize(final String str) { + return str.substring(0, 1).toUpperCase() + str.substring(1); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java new file mode 100644 index 000000000..52893510d --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/JavascriptTest.java @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.json.JSONObject; +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.tests.helpers.JavascriptBridge; +import org.mozilla.gecko.tests.helpers.JavascriptMessageParser; + +import android.util.Log; + +/** + * Extended to test stand-alone Javascript in automation. If you're looking to test JS interactions + * with Java, see {@link JavascriptBridgeTest}. + * + * There are also other tests that run stand-alone javascript but are more difficult for the mobile + * team to run (e.g. xpcshell). + */ +public class JavascriptTest extends BaseTest { + private static final String LOGTAG = "JavascriptTest"; + private static final String EVENT_TYPE = JavascriptBridge.EVENT_TYPE; + + // Calculate these once, at initialization. isLoggable is too expensive to + // have in-line in each log call. + private static final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); + private static final boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); + + private final String javascriptUrl; + + public JavascriptTest(String javascriptUrl) { + super(); + this.javascriptUrl = javascriptUrl; + } + + public void testJavascript() throws Exception { + blockForGeckoReady(); + + doTestJavascript(); + } + + protected void doTestJavascript() throws Exception { + // We want to be waiting for Robocop messages before the page is loaded + // because the test harness runs each test in the suite (and possibly + // completes testing) before the page load event is fired. + final Actions.EventExpecter expecter = mActions.expectGeckoEvent(EVENT_TYPE); + mAsserter.dumpLog("Registered listener for " + EVENT_TYPE); + + final String url = getAbsoluteUrl(mStringHelper.getHarnessUrlForJavascript(javascriptUrl)); + mAsserter.dumpLog("Loading JavaScript test from " + url); + loadUrl(url); + + final JavascriptMessageParser testMessageParser = + new JavascriptMessageParser(mAsserter, false); + try { + while (!testMessageParser.isTestFinished()) { + if (logVerbose) { + Log.v(LOGTAG, "Waiting for " + EVENT_TYPE); + } + String data = expecter.blockForEventData(); + if (logVerbose) { + Log.v(LOGTAG, "Got event with data '" + data + "'"); + } + + JSONObject o = new JSONObject(data); + String innerType = o.getString("innerType"); + if (!"progress".equals(innerType)) { + throw new Exception("Unexpected event innerType " + innerType); + } + + String message = o.getString("message"); + if (message == null) { + throw new Exception("Progress message must not be null"); + } + testMessageParser.logMessage(message); + } + + if (logDebug) { + Log.d(LOGTAG, "Got test finished message"); + } + } finally { + expecter.unregisterListener(); + mAsserter.dumpLog("Unregistered listener for " + EVENT_TYPE); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java new file mode 100644 index 000000000..5b8254e99 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventHelper.java @@ -0,0 +1,210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.PrefsHelper; + +import android.app.Instrumentation; +import android.os.SystemClock; +import android.util.Log; +import android.view.MotionEvent; + +class MotionEventHelper { + private static final String LOGTAG = "RobocopMotionEventHelper"; + + private static final long DRAG_EVENTS_PER_SECOND = 20; // 20 move events per second when doing a drag + + private final Instrumentation mInstrumentation; + private final int mSurfaceOffsetX; + private final int mSurfaceOffsetY; + private final LayerView layerView; + private boolean mApzEnabled; + private float mTouchStartTolerance; + private final int mDpi; + + public MotionEventHelper(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY) { + mInstrumentation = inst; + mSurfaceOffsetX = surfaceOffsetX; + mSurfaceOffsetY = surfaceOffsetY; + layerView = GeckoAppShell.getLayerView(); + mApzEnabled = false; + mTouchStartTolerance = 0.0f; + mDpi = GeckoAppShell.getDpi(); + Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")"); + PrefsHelper.getPref("layers.async-pan-zoom.enabled", new PrefsHelper.PrefHandlerBase() { + @Override public void prefValue(String pref, boolean value) { + mApzEnabled = value; + } + }); + PrefsHelper.getPref("apz.touch_start_tolerance", new PrefsHelper.PrefHandlerBase() { + @Override public void prefValue(String pref, String value) { + mTouchStartTolerance = Float.parseFloat(value); + } + }); + } + + public long down(float x, float y) { + Log.d(LOGTAG, "Triggering down at (" + x + "," + y + ")"); + long downTime = SystemClock.uptimeMillis(); + MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0); + try { + mInstrumentation.sendPointerSync(event); + } finally { + event.recycle(); + event = null; + } + return downTime; + } + + public long move(long downTime, float x, float y) { + return move(downTime, SystemClock.uptimeMillis(), x, y); + } + + public long move(long downTime, long moveTime, float x, float y) { + Log.d(LOGTAG, "Triggering move to (" + x + "," + y + ")"); + MotionEvent event = MotionEvent.obtain(downTime, moveTime, MotionEvent.ACTION_MOVE, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0); + try { + mInstrumentation.sendPointerSync(event); + } finally { + event.recycle(); + event = null; + } + return downTime; + } + + public long up(long downTime, float x, float y) { + return up(downTime, SystemClock.uptimeMillis(), x, y); + } + + public long up(long downTime, long upTime, float x, float y) { + Log.d(LOGTAG, "Triggering up at (" + x + "," + y + ")"); + MotionEvent event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, mSurfaceOffsetX + x, mSurfaceOffsetY + y, 0); + try { + mInstrumentation.sendPointerSync(event); + } finally { + event.recycle(); + event = null; + } + return -1L; + } + + private long movePastTouchStartTolerance(float startX, float startY, float endX, float endY) { + long downTime = 0; + float eventDx = (endX - startX); + float eventDy = (endY - startY); + if (mApzEnabled && (mTouchStartTolerance > 0.0f) && (eventDx != 0 || eventDy !=0)) { + final float dragLength = (float)Math.sqrt((eventDx * eventDx) + (eventDy * eventDy)); + final float extraDragLength = (float)Math.ceil(mTouchStartTolerance * mDpi); + final float extraDx = (eventDx / dragLength) * extraDragLength * (eventDx > 0.0f ? -1.0f : 1.0f); + final float extraDy = (eventDy / dragLength) * extraDragLength * (eventDy > 0.0f ? -1.0f : 1.0f); + downTime = down(startX + extraDx, startY + extraDy); + downTime = move(downTime, startX + extraDx, startY + extraDy); + try { + Thread.sleep(1000L / DRAG_EVENTS_PER_SECOND); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } else { + downTime = down(startX, startY); + } + return downTime; + } + + public Thread dragAsync(final float startX, final float startY, final float endX, final float endY, final long durationMillis) { + Thread t = new Thread() { + @Override + public void run() { + layerView.setIsLongpressEnabled(false); + + int numEvents = (int)(durationMillis * DRAG_EVENTS_PER_SECOND / 1000); + float eventDx = (endX - startX) / numEvents; + float eventDy = (endY - startY) / numEvents; + long downTime = movePastTouchStartTolerance(startX, startY, endX, endY); + for (int i = 0; i < numEvents - 1; i++) { + downTime = move(downTime, startX + (eventDx * i), startY + (eventDy * i)); + try { + Thread.sleep(1000L / DRAG_EVENTS_PER_SECOND); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + // sleep a bit before sending the last move so that the calculated + // fling velocity is low and we don't end up doing a fling afterwards. + try { + Thread.sleep(1000L); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + // do the last one using endX/endY directly to avoid rounding errors + downTime = move(downTime, endX, endY); + downTime = up(downTime, endX, endY); + + layerView.setIsLongpressEnabled(true); + } + }; + t.start(); + return t; + } + + public void dragSync(float startX, float startY, float endX, float endY, long durationMillis) { + try { + dragAsync(startX, startY, endX, endY, durationMillis).join(); + mInstrumentation.waitForIdleSync(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + + public void dragSync(float startX, float startY, float endX, float endY) { + dragSync(startX, startY, endX, endY, 1000); + } + + public Thread flingAsync(final float startX, final float startY, final float endX, final float endY, final float velocity) { + // note that the first move after the touch-down is used to get over the panning threshold, and + // is basically cancelled out. this means we need to generate (at least) two move events, with + // the last move event hitting the target velocity. to do this we just slice the total distance + // in half, assuming the first half will get us over the panning threshold and the second half + // will trigger the fling. + final float dx = (endX - startX) / 2; + final float dy = (endY - startY) / 2; + float distance = (float) Math.sqrt((dx * dx) + (dy * dy)); + final long time = (long)(distance / velocity); + if (time <= 0) { + throw new IllegalArgumentException( "Fling parameters require too small a time period" ); + } + Thread t = new Thread() { + @Override + public void run() { + long downTime = down(startX, startY); + downTime = move(downTime, downTime + time, startX + dx, startY + dy); + downTime = move(downTime, downTime + time + time, endX, endY); + downTime = up(downTime, downTime + time + time + time, endX, endY); + } + }; + t.start(); + return t; + } + + public void flingSync(float startX, float startY, float endX, float endY, float velocity) { + try { + flingAsync(startX, startY, endX, endY, velocity).join(); + mInstrumentation.waitForIdleSync(); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + + public void tap(float x, float y) { + long downTime = down(x, y); + downTime = up(downTime, x, y); + } + + public void doubleTap(float x, float y) { + tap(x, y); + tap(x, y); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java new file mode 100644 index 000000000..508c6b197 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/MotionEventReplayer.java @@ -0,0 +1,224 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.app.Instrumentation; +import android.os.Build; +import android.os.SystemClock; +import android.util.Log; +import android.view.MotionEvent; + +class MotionEventReplayer { + private static final String LOGTAG = "RobocopMotionEventReplayer"; + + // the inner dimensions of the window on which the motion event capture was taken from + private static final int CAPTURE_WINDOW_WIDTH = 720; + private static final int CAPTURE_WINDOW_HEIGHT = 1038; + + private final Instrumentation mInstrumentation; + private final int mSurfaceOffsetX; + private final int mSurfaceOffsetY; + private final int mSurfaceWidth; + private final int mSurfaceHeight; + private final Map<String, Integer> mActionTypes; + private Method mObtainNanoMethod; + + public MotionEventReplayer(Instrumentation inst, int surfaceOffsetX, int surfaceOffsetY, int surfaceWidth, int surfaceHeight) { + mInstrumentation = inst; + mSurfaceOffsetX = surfaceOffsetX; + mSurfaceOffsetY = surfaceOffsetY; + mSurfaceWidth = surfaceWidth; + mSurfaceHeight = surfaceHeight; + Log.i(LOGTAG, "Initialized using offset (" + mSurfaceOffsetX + "," + mSurfaceOffsetY + ")"); + + mActionTypes = new HashMap<String, Integer>(); + mActionTypes.put("ACTION_CANCEL", MotionEvent.ACTION_CANCEL); + mActionTypes.put("ACTION_DOWN", MotionEvent.ACTION_DOWN); + mActionTypes.put("ACTION_MOVE", MotionEvent.ACTION_MOVE); + mActionTypes.put("ACTION_POINTER_DOWN", MotionEvent.ACTION_POINTER_DOWN); + mActionTypes.put("ACTION_POINTER_UP", MotionEvent.ACTION_POINTER_UP); + mActionTypes.put("ACTION_UP", MotionEvent.ACTION_UP); + } + + private int parseAction(String action) { + int index = 0; + + // ACTION_POINTER_DOWN and ACTION_POINTER_UP might be followed by + // pointer index in parentheses, like ACTION_POINTER_UP(1) + int beginParen = action.indexOf("("); + if (beginParen >= 0) { + int endParen = action.indexOf(")", beginParen + 1); + index = Integer.parseInt(action.substring(beginParen + 1, endParen)); + action = action.substring(0, beginParen); + } + + return mActionTypes.get(action) | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT); + } + + private int parseInt(String value) { + if (value == null) { + return 0; + } + if (value.startsWith("0x")) { + return Integer.parseInt(value.substring(2), 16); + } + return Integer.parseInt(value); + } + + private float scaleX(float value) { + return value * mSurfaceWidth / CAPTURE_WINDOW_WIDTH; + } + + private float scaleY(float value) { + return value * mSurfaceHeight / CAPTURE_WINDOW_HEIGHT; + } + + public void replayEvents(InputStream eventDescriptions) + throws IOException, IllegalAccessException, InvocationTargetException, NoSuchMethodException + { + // As an example, a line in the input stream might look like: + // + // MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=424.41055, y[0]=825.2412, + // toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, + // edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=21972329, + // downTime=21972329, deviceId=6, source=0x1002 } + // + // These can be generated by printing out event.toString() in LayerView's + // onTouchEvent function on a phone running Ice Cream Sandwich. Different + // Android versions have different serializations of the motion event, and this + // code could probably be modified to parse other serializations if needed. + Pattern p = Pattern.compile("MotionEvent \\{ (.*?) \\}"); + Map<String, String> eventProperties = new HashMap<String, String>(); + + boolean firstEvent = true; + long timeDelta = 0L; + long lastEventTime = 0L; + + BufferedReader br = new BufferedReader(new InputStreamReader(eventDescriptions)); + try { + for (String eventStr = br.readLine(); eventStr != null; eventStr = br.readLine()) { + Matcher m = p.matcher(eventStr); + if (! m.find()) { + // this line doesn't have any MotionEvent data, skip it + continue; + } + + // extract the key-value pairs from the description and store them + // in the eventProperties table + StringTokenizer keyValues = new StringTokenizer(m.group(1), ","); + while (keyValues.hasMoreTokens()) { + String keyValue = keyValues.nextToken(); + String key = keyValue.substring(0, keyValue.indexOf('=')).trim(); + String value = keyValue.substring(keyValue.indexOf('=') + 1).trim(); + eventProperties.put(key, value); + } + + // set up the values we need to build the MotionEvent + long downTime = Long.parseLong(eventProperties.get("downTime")); + long eventTime = Long.parseLong(eventProperties.get("eventTime")); + int action = parseAction(eventProperties.get("action")); + float pressure = 1.0f; + float size = 1.0f; + int metaState = parseInt(eventProperties.get("metaState")); + float xPrecision = 1.0f; + float yPrecision = 1.0f; + int deviceId = 0; + int edgeFlags = parseInt(eventProperties.get("edgeFlags")); + int source = parseInt(eventProperties.get("source")); + int flags = parseInt(eventProperties.get("flags")); + + int pointerCount = parseInt(eventProperties.get("pointerCount")); + int[] pointerIds = new int[pointerCount]; + Object pointerData; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; + for (int i = 0; i < pointerCount; i++) { + pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]")); + pointerCoords[i] = new MotionEvent.PointerCoords(); + pointerCoords[i].x = mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); + pointerCoords[i].y = mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]"))); + } + pointerData = pointerCoords; + } else { + // pre-gingerbread we have to use a hidden API to create the motion event, and we have + // to create a flattened list of floats rather than an array of PointerCoords + final int NUM_SAMPLE_DATA = 4; // MotionEvent.NUM_SAMPLE_DATA + final int SAMPLE_X = 0; // MotionEvent.SAMPLE_X + final int SAMPLE_Y = 1; // MotionEvent.SAMPLE_Y + float[] sampleData = new float[pointerCount * NUM_SAMPLE_DATA]; + for (int i = 0; i < pointerCount; i++) { + pointerIds[i] = Integer.parseInt(eventProperties.get("id[" + i + "]")); + sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_X] = + mSurfaceOffsetX + scaleX(Float.parseFloat(eventProperties.get("x[" + i + "]"))); + sampleData[(i * NUM_SAMPLE_DATA) + SAMPLE_Y] = + mSurfaceOffsetY + scaleY(Float.parseFloat(eventProperties.get("y[" + i + "]"))); + } + pointerData = sampleData; + } + + // we want to adjust the timestamps on all the generated events so that they line up with + // the time that this function is executing on-device. + long now = SystemClock.uptimeMillis(); + if (firstEvent) { + timeDelta = now - eventTime; + firstEvent = false; + } + downTime += timeDelta; + eventTime += timeDelta; + + // we also generate the events in "real-time" (i.e. have delays between events that + // correspond to the delays in the event timestamps). + if (now < eventTime) { + try { + Thread.sleep(eventTime - now); + } catch (InterruptedException ie) { + } + } + + // and finally we dispatch the event + MotionEvent event; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + event = MotionEvent.obtain(downTime, eventTime, action, pointerCount, + pointerIds, (MotionEvent.PointerCoords[])pointerData, metaState, + xPrecision, yPrecision, deviceId, edgeFlags, source, flags); + } else { + // pre-gingerbread we have to use a hidden API to accomplish this + if (mObtainNanoMethod == null) { + mObtainNanoMethod = MotionEvent.class.getMethod("obtainNano", long.class, + long.class, long.class, int.class, int.class, pointerIds.getClass(), + pointerData.getClass(), int.class, float.class, float.class, + int.class, int.class); + } + event = (MotionEvent)mObtainNanoMethod.invoke(null, downTime, eventTime, + eventTime * 1000000, action, pointerCount, pointerIds, (float[])pointerData, + metaState, xPrecision, yPrecision, deviceId, edgeFlags); + } + try { + Log.v(LOGTAG, "Injecting " + event.toString()); + mInstrumentation.sendPointerSync(event); + } finally { + event.recycle(); + event = null; + } + + eventProperties.clear(); + } + } finally { + br.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java new file mode 100644 index 000000000..a33ecf241 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/PixelTest.java @@ -0,0 +1,117 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.PaintedSurface; + +abstract class PixelTest extends BaseTest { + private static final long PAINT_CLEAR_DELAY = 10000; // milliseconds + + protected final PaintedSurface loadAndGetPainted(String url) { + Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint(); + loadUrlAndWait(url); + verifyHomePagerHidden(); + paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY); + paintExpecter.unregisterListener(); + PaintedSurface p = mDriver.getPaintedSurface(); + if (p == null) { + mAsserter.ok(p != null, "checking that painted surface loaded", + "painted surface loaded"); + } + return p; + } + + protected final void loadAndPaint(String url) { + PaintedSurface painted = loadAndGetPainted(url); + painted.close(); + } + + protected final PaintedSurface reloadAndGetPainted() { + Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint(); + + mActions.sendSpecialKey(Actions.SpecialKey.MENU); + waitForText(mStringHelper.RELOAD_LABEL); + mSolo.clickOnText(mStringHelper.RELOAD_LABEL); + + paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY); + paintExpecter.unregisterListener(); + PaintedSurface p = mDriver.getPaintedSurface(); + if (p == null) { + mAsserter.ok(p != null, "checking that painted surface loaded", + "painted surface loaded"); + } + return p; + } + + protected final void reloadAndPaint() { + PaintedSurface painted = reloadAndGetPainted(); + painted.close(); + } + + protected final PaintedSurface waitForPaint(Actions.RepeatedEventExpecter expecter) { + expecter.blockUntilClear(PAINT_CLEAR_DELAY); + PaintedSurface p = mDriver.getPaintedSurface(); + if (p == null) { + mAsserter.ok(p != null, "checking that painted surface loaded", + "painted surface loaded"); + } + return p; + } + + protected final PaintedSurface waitWithNoPaint(Actions.RepeatedEventExpecter expecter) { + try { + Thread.sleep(PAINT_CLEAR_DELAY); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + mAsserter.is(expecter.eventReceived(), false, "Checking gecko didn't draw unnecessarily"); + PaintedSurface p = mDriver.getPaintedSurface(); + if (p == null) { + mAsserter.ok(p != null, "checking that painted surface loaded", + "painted surface loaded"); + } + return p; + } + + // this matches the algorithm in robocop_boxes.html + protected final int[] getBoxColorAt(int x, int y) { + int r = ((int)Math.floor(x / 3) % 256); + r = r & 0xF8; + int g = (x + y) % 256; + g = g & 0xFC; + int b = ((int)Math.floor(y / 3) % 256); + b = b & 0xF8; + return new int[] { r, g, b }; + } + + /** + * Checks the top-left corner of the visible area of the page is at (x,y) of robocop_boxes.html. + */ + protected final void checkScrollWithBoxes(PaintedSurface painted, int x, int y) { + int[] color = getBoxColorAt(x, y); + mAsserter.ispixel(painted.getPixelAt(0, 0), color[0], color[1], color[2], "Pixel at 0, 0"); + color = getBoxColorAt(x + 100, y); + mAsserter.ispixel(painted.getPixelAt(100, 0), color[0], color[1], color[2], "Pixel at 100, 0"); + color = getBoxColorAt(x, y + 100); + mAsserter.ispixel(painted.getPixelAt(0, 100), color[0], color[1], color[2], "Pixel at 0, 100"); + color = getBoxColorAt(x + 100, y + 100); + mAsserter.ispixel(painted.getPixelAt(100, 100), color[0], color[1], color[2], "Pixel at 100, 100"); + } + + /** + * Loads the robocop_boxes.html file and verifies that we are positioned at (0,0) on it. + * @param url URL of the robocop_boxes.html file. + * @return The painted surface after rendering the file. + */ + protected final void loadAndVerifyBoxes(String url) { + PaintedSurface painted = loadAndGetPainted(url); + try { + checkScrollWithBoxes(painted, 0, 0); + } finally { + painted.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java new file mode 100644 index 000000000..eb808b542 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SelectionHandlerTest.java @@ -0,0 +1,56 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +import android.util.Log; + +import org.json.JSONObject; + +/** + * A base test class for selection handler tests. + */ +abstract class SelectionHandlerTest extends UITest { + private static final String geckoEventString = "Robocop:testSelectionHandler"; + private final String url; + + public SelectionHandlerTest(String url) { + this.url = url; + } + + public void testSelection() { + GeckoHelper.blockForReady(); + + Actions.EventExpecter robocopTestExpecter = getActions().expectGeckoEvent(geckoEventString); + NavigationHelper.enterAndLoadUrl(url); + mToolbar.assertTitle(url); + + while (!test(robocopTestExpecter)) { + // do nothing + } + + robocopTestExpecter.unregisterListener(); + } + + protected boolean test(Actions.EventExpecter expecter) { + final JSONObject eventData; + try { + eventData = new JSONObject(expecter.blockForEventData()); + } catch(Exception ex) { + // Log and ignore + getAsserter().ok(false, "JS Test", "Error decoding data " + ex); + return false; + } + + if (eventData.has("result")) { + getAsserter().ok(eventData.optBoolean("result"), "JS Test", eventData.optString("msg")); + } else if (eventData.has("todo")) { + getAsserter().todo(eventData.optBoolean("todo"), "JS TODO", eventData.optString("msg")); + } + + EventDispatcher.sendResponse(eventData, new JSONObject()); + return eventData.optBoolean("done", false); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java new file mode 100644 index 000000000..e07a9750c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/SessionTest.java @@ -0,0 +1,407 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Assert; +import org.mozilla.gecko.FennecMochitestAssert; + +public abstract class SessionTest extends BaseTest { + protected Navigation mNavigation; + + @Override + public void setUp() throws Exception { + super.setUp(); + + mNavigation = new Navigation(mDevice); + } + + /** + * A generic session object representing a collection of items that has a + * selected index. + */ + protected abstract class SessionObject<T> { + private final int mIndex; + private final T[] mItems; + + public SessionObject(int index, T... items) { + mIndex = index; + mItems = items; + } + + public int getIndex() { + return mIndex; + } + + public T[] getItems() { + return mItems; + } + } + + protected class PageInfo { + private final String url; + private final String title; + + public PageInfo(String key) { + if (key.startsWith("about:")) { + url = key; + } else { + url = getPage(key); + } + title = key; + } + } + + protected class SessionTab extends SessionObject<PageInfo> { + public SessionTab(int index, PageInfo... items) { + super(index, items); + } + } + + protected class Session extends SessionObject<SessionTab> { + public Session(int index, SessionTab... items) { + super(index, items); + } + } + + /** + * Walker for visiting items in a browser-like navigation order. + */ + protected abstract class NavigationWalker<T> { + private final T[] mItems; + private final int mIndex; + + public NavigationWalker(SessionObject<T> obj) { + mItems = obj.getItems(); + mIndex = obj.getIndex(); + } + + /** + * Walks over the list of items, calling the onItem() callback for each. + * + * The selected item is the first item visited. Each item after the + * selected item is then visited in ascending index order. Finally, the + * list is iterated in reverse, and each item before the selected item + * is visited in descending index order. + */ + public void walk() { + onItem(mItems[mIndex], mIndex); + for (int i = mIndex + 1; i < mItems.length; i++) { + goForward(); + onItem(mItems[i], i); + } + if (mIndex > 0) { + for (int i = mItems.length - 2; i >= 0; i--) { + goBack(); + if (i < mIndex) { + onItem(mItems[i], i); + } + } + } + } + + /** + * Callback when an item is visited during a walk. + * + * Only one callback is executed per item. + */ + public abstract void onItem(T item, int currentIndex); + + /** + * Callback executed for each back step of the walk. + */ + public void goBack() {} + + /** + * Callback executed for each forward step of the walk. + */ + public void goForward() {} + } + + /** + * Loads a set of tabs in the browser specified by the given session. + * + * @param session Session to load + */ + protected void loadSessionTabs(Session session) { + // Verify initial about:home tab + verifyTabCount(1); + verifyUrl(mStringHelper.ABOUT_HOME_URL); + + SessionTab[] tabs = session.getItems(); + for (int i = 0; i < tabs.length; i++) { + final SessionTab tab = tabs[i]; + final PageInfo[] pages = tab.getItems(); + + // New tabs always start with about:home, so make sure about:home + // is always the first entry. + mAsserter.is(pages[0].url, mStringHelper.ABOUT_HOME_URL, "first page in tab is " + + mStringHelper.ABOUT_HOME_URL); + + // If this is the first tab, the tab already exists, so no need to + // create a new one. Otherwise, create a new tab if we're loading + // the first the first page in the set. + if (i > 0) { + addTab(); + } + + for (int j = 1; j < pages.length; j++) { + Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); + + loadUrl(pages[j].url); + + pageShowExpecter.blockForEvent(); + pageShowExpecter.unregisterListener(); + } + + final int index = tab.getIndex(); + for (int j = pages.length - 1; j > index; j--) { + mNavigation.back(); + } + } + + selectTabAt(session.getIndex()); + } + + /** + * Verifies that the set of open tabs matches the given session. + * + * @param session Session to verify + */ + protected void verifySessionTabs(Session session) { + verifyTabCount(session.getItems().length); + + (new NavigationWalker<SessionTab>(session) { + boolean mFirstTabVisited; + + @Override + public void onItem(SessionTab tab, int currentIndex) { + // The first tab to check should already be selected at startup + if (mFirstTabVisited) { + selectTabAt(currentIndex); + } else { + mFirstTabVisited = true; + } + + (new NavigationWalker<PageInfo>(tab) { + @Override + public void onItem(PageInfo page, int currentIndex) { + final String text; + if (mStringHelper.ABOUT_HOME_URL.equals(page.url)) { + text = mStringHelper.TITLE_PLACE_HOLDER; + } else if (page.url.startsWith(URL_HTTP_PREFIX)) { + text = page.url.substring(URL_HTTP_PREFIX.length()); + } else { + text = page.url; + } + waitForText(text); + + verifyUrlBarTitle(page.url); + } + + @Override + public void goBack() { + mNavigation.back(); + } + + @Override + public void goForward() { + mNavigation.forward(); + } + }).walk(); + } + }).walk(); + } + + /** + * Gets session restore JSON corresponding to the open session. + * + * The JSON format follows the format used in Gecko for session restore and + * should be interchangeable with the Gecko's generated sessionstore.js. + * + * @param session Session to serialize + * @return JSON string of session + */ + protected String buildSessionJSON(Session session) { + final SessionTab[] sessionTabs = session.getItems(); + String sessionString = null; + + try { + final JSONArray tabs = new JSONArray(); + + for (int i = 0; i < sessionTabs.length; i++) { + final JSONObject tab = new JSONObject(); + final JSONArray entries = new JSONArray(); + final SessionTab sessionTab = sessionTabs[i]; + final PageInfo[] pages = sessionTab.getItems(); + + for (int j = 0; j < pages.length; j++) { + final PageInfo page = pages[j]; + final JSONObject entry = new JSONObject(); + entry.put("url", page.url); + entry.put("title", page.title); + entries.put(entry); + } + + tab.put("entries", entries); + tab.put("index", sessionTab.getIndex() + 1); + tabs.put(tab); + } + + JSONObject window = new JSONObject(); + window.put("tabs", tabs); + window.put("selected", session.getIndex() + 1); + sessionString = new JSONObject().put("windows", new JSONArray().put(window)).toString(); + } catch (JSONException e) { + mAsserter.ok(false, "JSON exception", getStackTraceString(e)); + } + + return sessionString; + } + + /** + * @see SessionTest#verifySessionJSON(Session, String, Assert) + */ + protected void verifySessionJSON(Session session, String sessionString) { + verifySessionJSON(session, sessionString, mAsserter); + } + + /** + * Verifies a session JSON string against the given session. + * + * @param session Session to verify against + * @param sessionString JSON string to verify + * @param asserter Assert class to use during verification + */ + protected void verifySessionJSON(Session session, String sessionString, Assert asserter) { + final SessionTab[] sessionTabs = session.getItems(); + + try { + final JSONObject window = new JSONObject(sessionString).getJSONArray("windows").getJSONObject(0); + final JSONArray tabs = window.getJSONArray("tabs"); + final int optSelected = window.optInt("selected", -1); + + asserter.is(optSelected, session.getIndex() + 1, "selected tab matches"); + + for (int i = 0; i < tabs.length(); i++) { + final JSONObject tab = tabs.getJSONObject(i); + final int index = tab.getInt("index"); + final JSONArray entries = tab.getJSONArray("entries"); + final SessionTab sessionTab = sessionTabs[i]; + final PageInfo[] pages = sessionTab.getItems(); + + asserter.is(index, sessionTab.getIndex() + 1, "selected page index matches"); + + for (int j = 0; j < entries.length(); j++) { + final JSONObject entry = entries.getJSONObject(j); + final String url = entry.getString("url"); + final String title = entry.optString("title"); + final PageInfo page = pages[j]; + + asserter.is(url, page.url, "URL in JSON matches session URL"); + if (!page.url.startsWith("about:")) { + asserter.is(title, page.title, "title in JSON matches session title"); + } + } + } + } catch (JSONException e) { + asserter.ok(false, "JSON exception", getStackTraceString(e)); + } + } + + /** + * Exception thrown by NonFatalAsserter for assertion failures. + */ + public static class AssertException extends RuntimeException { + public AssertException(String msg) { + super(msg); + } + } + + /** + * Asserter that throws an AssertException on failure instead of aborting + * the test. + * + * This can be used in methods called via waitForCondition() where an assertion + * might not immediately succeed. + */ + public class NonFatalAsserter extends FennecMochitestAssert { + @Override + public void ok(boolean condition, String name, String diag) { + if (!condition) { + String details = (diag == null ? "" : " | " + diag); + throw new AssertException("Assertion failed: " + name + details); + } + mAsserter.ok(condition, name, diag); + } + } + + /** + * Gets a URL for a dynamically-generated page. + * + * The page will have a URL unique to the given ID, and the page's title + * will match the given ID. + * + * @param id ID used to generate page URL + * @return URL of the page + */ + protected String getPage(String id) { + return getAbsoluteUrl("/robocop/robocop_dynamic.sjs?id=" + id); + } + + protected String readProfileFile(String filename) { + try { + return readFile(new File(mProfile, filename)); + } catch (IOException e) { + mAsserter.ok(false, "Error reading" + filename, getStackTraceString(e)); + } + return null; + } + + protected void writeProfileFile(String filename, String data) { + try { + writeFile(new File(mProfile, filename), data); + } catch (IOException e) { + mAsserter.ok(false, "Error writing to " + filename, getStackTraceString(e)); + } + } + + private String readFile(File target) throws IOException { + if (!target.exists()) { + return null; + } + + FileReader fr = new FileReader(target); + try { + StringBuffer sb = new StringBuffer(); + char[] buf = new char[8192]; + int read = fr.read(buf); + while (read >= 0) { + sb.append(buf, 0, read); + read = fr.read(buf); + } + return sb.toString(); + } finally { + fr.close(); + } + } + + private void writeFile(File target, String data) throws IOException { + FileWriter writer = new FileWriter(target); + try { + writer.write(data); + } finally { + writer.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java new file mode 100644 index 000000000..6f5db560d --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java @@ -0,0 +1,401 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.content.res.Resources; + +import org.mozilla.gecko.R; + +public class StringHelper { + private static StringHelper instance; + + // This needs to be accessed statically, before an instance of StringHelper can be created. + public static String STATIC_ABOUT_HOME_URL = "about:home"; + + public final String OK; + public final String CANCEL; + public final String CLEAR; + + // Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length + public final String[] DEFAULT_BOOKMARKS_TITLES; + public final String[] DEFAULT_BOOKMARKS_URLS; + public final int DEFAULT_BOOKMARKS_COUNT; + + // About pages + public final String ABOUT_BLANK_URL = "about:blank"; + public final String ABOUT_FIREFOX_URL; + public final String ABOUT_HOME_URL = "about:home"; + public final String ABOUT_ADDONS_URL = "about:addons"; + public final String ABOUT_SCHEME = "about:"; + + // About pages' titles + public final String ABOUT_HOME_TITLE = ""; + + // Context Menu item strings + public final String CONTEXT_MENU_BOOKMARK_LINK = "Bookmark Link"; + public final String CONTEXT_MENU_OPEN_LINK_IN_NEW_TAB = "Open Link in New Tab"; + public final String CONTEXT_MENU_OPEN_IN_NEW_TAB; + public final String CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB = "Open Link in Private Tab"; + public final String CONTEXT_MENU_OPEN_IN_PRIVATE_TAB; + public final String CONTEXT_MENU_COPY_LINK = "Copy Link"; + public final String CONTEXT_MENU_SHARE_LINK = "Share Link"; + public final String CONTEXT_MENU_EDIT; + public final String CONTEXT_MENU_SHARE; + public final String CONTEXT_MENU_REMOVE; + public final String CONTEXT_MENU_COPY_ADDRESS; + public final String CONTEXT_MENU_EDIT_SITE_SETTINGS; + public final String CONTEXT_MENU_SITE_SETTINGS_SAVE_PASSWORD = "Save Password"; + public final String CONTEXT_MENU_ADD_TO_HOME_SCREEN; + public final String CONTEXT_MENU_PIN_SITE; + public final String CONTEXT_MENU_UNPIN_SITE; + + // Context Menu menu items + public final String[] CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB; + + public final String[] CONTEXT_MENU_ITEMS_IN_NORMAL_TAB; + + public final String[] BOOKMARK_CONTEXT_MENU_ITEMS; + + public final String[] CONTEXT_MENU_ITEMS_IN_URL_BAR; + + public final String TITLE_PLACE_HOLDER; + + // Robocop page urls + // Note: please use getAbsoluteUrl(String url) on each robocop url to get the correct url + public final String ROBOCOP_BIG_LINK_URL = "/robocop/robocop_big_link.html"; + public final String ROBOCOP_BIG_MAILTO_URL = "/robocop/robocop_big_mailto.html"; + public final String ROBOCOP_BLANK_PAGE_01_URL = "/robocop/robocop_blank_01.html"; + public final String ROBOCOP_BLANK_PAGE_02_URL = "/robocop/robocop_blank_02.html"; + public final String ROBOCOP_BLANK_PAGE_03_URL = "/robocop/robocop_blank_03.html"; + public final String ROBOCOP_BLANK_PAGE_04_URL = "/robocop/robocop_blank_04.html"; + public final String ROBOCOP_BLANK_PAGE_05_URL = "/robocop/robocop_blank_05.html"; + public final String ROBOCOP_BOXES_URL = "/robocop/robocop_boxes.html"; + public final String ROBOCOP_GEOLOCATION_URL = "/robocop/robocop_geolocation.html"; + public final String ROBOCOP_LOGIN_01_URL= "/robocop/robocop_login_01.html"; + public final String ROBOCOP_LOGIN_02_URL= "/robocop/robocop_login_02.html"; + public final String ROBOCOP_POPUP_URL = "/robocop/robocop_popup.html"; + public final String ROBOCOP_OFFLINE_STORAGE_URL = "/robocop/robocop_offline_storage.html"; + public final String ROBOCOP_PICTURE_LINK_URL = "/robocop/robocop_picture_link.html"; + public final String ROBOCOP_SEARCH_URL = "/robocop/robocop_search.html"; + public final String ROBOCOP_TEXT_PAGE_URL = "/robocop/robocop_text_page.html"; + public final String ROBOCOP_ADOBE_FLASH_URL = "/robocop/robocop_adobe_flash.html"; + public final String ROBOCOP_INPUT_URL = "/robocop/robocop_input.html"; + public final String ROBOCOP_READER_MODE_BASIC_ARTICLE = "/robocop/reader_mode_pages/basic_article.html"; + public final String ROBOCOP_LINK_TO_SLOW_LOADING = "/robocop/robocop_link_to_slow_loading.html"; + + private final String ROBOCOP_JS_HARNESS_URL = "/robocop/robocop_javascript.html"; + + // Robocop page images + public final String ROBOCOP_PICTURE_URL = "/robocop/Firefox.jpg"; + + // Robocop page titles + public final String ROBOCOP_BIG_LINK_TITLE = "Big Link"; + public final String ROBOCOP_BIG_MAILTO_TITLE = "Big Mailto"; + public final String ROBOCOP_BLANK_PAGE_01_TITLE = "Browser Blank Page 01"; + public final String ROBOCOP_BLANK_PAGE_02_TITLE = "Browser Blank Page 02"; + public final String ROBOCOP_GEOLOCATION_TITLE = "Geolocation Test Page"; + public final String ROBOCOP_PICTURE_LINK_TITLE = "Picture Link"; + public final String ROBOCOP_SEARCH_TITLE = "Robocop Search Engine"; + + // Distribution tile labels + public final String DISTRIBUTION1_LABEL = "Distribution 1"; + public final String DISTRIBUTION2_LABEL = "Distribution 2"; + + // Settings menu strings + public final String PRIVACY_SECTION_LABEL; + public final String MOZILLA_SECTION_LABEL; + + // Mozilla + public final String BRAND_NAME = "(Fennec|Nightly|Firefox Aurora|Firefox Beta|Firefox)"; + public final String ABOUT_LABEL = "About " + BRAND_NAME ; + public final String LOCATION_SERVICES_LABEL = "Mozilla Location Service"; + + // Labels for the about:home tabs + public final String HISTORY_LABEL; + public final String TOP_SITES_LABEL; + public final String BOOKMARKS_LABEL; + public final String TODAY_LABEL; + + // Desktop default bookmarks folders + public final String BOOKMARKS_UP_TO; + public final String BOOKMARKS_ROOT_LABEL; + public final String DESKTOP_FOLDER_LABEL; + public final String TOOLBAR_FOLDER_LABEL; + public final String BOOKMARKS_MENU_FOLDER_LABEL; + public final String UNSORTED_FOLDER_LABEL; + + // Menu items - some of the items are found only on android 2.3 and lower and some only on android 3.0+ + public final String NEW_TAB_LABEL; + public final String NEW_PRIVATE_TAB_LABEL; + public final String SHARE_LABEL; + public final String FIND_IN_PAGE_LABEL; + public final String DESKTOP_SITE_LABEL; + public final String PDF_LABEL; + public final String DOWNLOADS_LABEL; + public final String ADDONS_LABEL; + public final String LOGINS_LABEL; + public final String SETTINGS_LABEL; + public final String GUEST_MODE_LABEL; + public final String TAB_QUEUE_LABEL; + public final String TAB_QUEUE_SUMMARY; + + // Android 3.0+ + public final String TOOLS_LABEL; + public final String PAGE_LABEL; + + // Android 2.3 and lower only + public final String MORE_LABEL = "More"; + public final String RELOAD_LABEL; + public final String FORWARD_LABEL; + public final String BOOKMARK_LABEL; + + // Bookmark Toast Notification + public final String BOOKMARK_ADDED_LABEL; + public final String BOOKMARK_REMOVED_LABEL; + public final String BOOKMARK_UPDATED_LABEL; + public final String BOOKMARK_OPTIONS_LABEL; + + // Edit Bookmark screen + public final String EDIT_BOOKMARK; + + // Strings used in doorhanger messages and buttons + public final String GEO_MESSAGE = "Share your location with"; + public final String GEO_ALLOW; + public final String GEO_DENY = "Don't share"; + + public final String OFFLINE_MESSAGE = "to store data on your device for offline use"; + public final String OFFLINE_ALLOW = "Allow"; + public final String OFFLINE_DENY = "Don't allow"; + + public final String LOGIN_MESSAGE = "Would you like " + BRAND_NAME + " to remember this login?"; + public final String LOGIN_ALLOW = "Remember"; + public final String LOGIN_DENY = "Never"; + + public final String POPUP_MESSAGE = "prevented this site from opening"; + public final String POPUP_ALLOW; + public final String POPUP_DENY = "Don't show"; + + // Strings used as content description, e.g. for ImageButtons + public final String CONTENT_DESCRIPTION_READER_MODE_BUTTON = "Enter Reader View"; + + // Home Panel Settings + public final String CUSTOMIZE_HOME; + public final String ENABLED; + public final String HISTORY; + public final String PANELS; + + // Search Settings + public final String SEARCH_TITLE; + public final String SEARCH_SUGGESTIONS; + public final String SEARCH_INSTALLED; + + // Advanced Settings + public final String ADVANCED; + public final String DONT_SHOW_MENU; + public final String SHOW_MENU; + public final String DISABLED; + public final String TAP_TO_PLAY; + public final String HIDE_TITLE_BAR; + + // Update Settings + public final String AUTOMATIC_UPDATES; + public final String OVER_WIFI_OPTION; + public final String DOWNLOAD_UPDATES_AUTO; + public final String ALWAYS; + public final String NEVER; + + // Restore Tabs Settings + public final String DONT_RESTORE_TABS; + public final String ALWAYS_RESTORE_TABS; + public final String DONT_RESTORE_QUIT; + + private StringHelper(final Resources res) { + + OK = res.getString(R.string.button_ok); + CANCEL = res.getString(R.string.button_cancel); + CLEAR = res.getString(R.string.button_clear); + + // Note: DEFAULT_BOOKMARKS_TITLES.length == DEFAULT_BOOKMARKS_URLS.length + DEFAULT_BOOKMARKS_TITLES = new String[] { + res.getString(R.string.bookmarkdefaults_title_aboutfirefox), + res.getString(R.string.bookmarkdefaults_title_support), + res.getString(R.string.bookmarkdefaults_title_addons) + }; + DEFAULT_BOOKMARKS_URLS = new String[] { + res.getString(R.string.bookmarkdefaults_url_aboutfirefox), + res.getString(R.string.bookmarkdefaults_url_support), + res.getString(R.string.bookmarkdefaults_url_addons) + }; + DEFAULT_BOOKMARKS_COUNT = DEFAULT_BOOKMARKS_TITLES.length; + + // About pages + ABOUT_FIREFOX_URL = res.getString(R.string.bookmarkdefaults_url_aboutfirefox); + + // Context Menu item strings + CONTEXT_MENU_OPEN_IN_NEW_TAB = res.getString(R.string.contextmenu_open_new_tab); + CONTEXT_MENU_OPEN_IN_PRIVATE_TAB = res.getString(R.string.contextmenu_open_private_tab); + CONTEXT_MENU_EDIT = res.getString(R.string.contextmenu_top_sites_edit); + CONTEXT_MENU_SHARE = res.getString(R.string.contextmenu_share); + CONTEXT_MENU_REMOVE = res.getString(R.string.contextmenu_remove); + CONTEXT_MENU_COPY_ADDRESS = res.getString(R.string.contextmenu_copyurl); + CONTEXT_MENU_EDIT_SITE_SETTINGS = res.getString(R.string.contextmenu_site_settings); + CONTEXT_MENU_ADD_TO_HOME_SCREEN = res.getString(R.string.contextmenu_add_to_launcher); + CONTEXT_MENU_PIN_SITE = res.getString(R.string.contextmenu_top_sites_pin); + CONTEXT_MENU_UNPIN_SITE = res.getString(R.string.contextmenu_top_sites_unpin); + + // Context Menu menu items + CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB = new String[] { + CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB, + CONTEXT_MENU_COPY_LINK, + CONTEXT_MENU_SHARE_LINK, + CONTEXT_MENU_BOOKMARK_LINK + }; + + CONTEXT_MENU_ITEMS_IN_NORMAL_TAB = new String[] { + CONTEXT_MENU_OPEN_LINK_IN_NEW_TAB, + CONTEXT_MENU_OPEN_LINK_IN_PRIVATE_TAB, + CONTEXT_MENU_COPY_LINK, + CONTEXT_MENU_SHARE_LINK, + CONTEXT_MENU_BOOKMARK_LINK + }; + + BOOKMARK_CONTEXT_MENU_ITEMS = new String[] { + CONTEXT_MENU_OPEN_IN_NEW_TAB, + CONTEXT_MENU_OPEN_IN_PRIVATE_TAB, + CONTEXT_MENU_COPY_ADDRESS, + CONTEXT_MENU_SHARE, + CONTEXT_MENU_EDIT, + CONTEXT_MENU_REMOVE, + CONTEXT_MENU_ADD_TO_HOME_SCREEN + }; + + CONTEXT_MENU_ITEMS_IN_URL_BAR = new String[] { + CONTEXT_MENU_SHARE, + CONTEXT_MENU_COPY_ADDRESS, + CONTEXT_MENU_EDIT_SITE_SETTINGS, + CONTEXT_MENU_ADD_TO_HOME_SCREEN + }; + + TITLE_PLACE_HOLDER = res.getString(R.string.url_bar_default_text); + + // Settings menu strings + PRIVACY_SECTION_LABEL = res.getString(R.string.pref_category_privacy_short); + MOZILLA_SECTION_LABEL = res.getString(R.string.pref_category_vendor); + + // Labels for the about:home tabs + HISTORY_LABEL = res.getString(R.string.home_history_title); + TOP_SITES_LABEL = res.getString(R.string.home_top_sites_title); + BOOKMARKS_LABEL = res.getString(R.string.bookmarks_title); + TODAY_LABEL = res.getString(R.string.history_today_section); + + BOOKMARKS_UP_TO = res.getString(R.string.home_move_back_to_filter); + BOOKMARKS_ROOT_LABEL = res.getString(R.string.bookmarks_title); + DESKTOP_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_desktop); + TOOLBAR_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_toolbar); + BOOKMARKS_MENU_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_menu); + UNSORTED_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_unfiled); + + // Menu items - some of the items are found only on android 2.3 and lower and some only on android 3.0+ + NEW_TAB_LABEL = res.getString(R.string.new_tab); + NEW_PRIVATE_TAB_LABEL = res.getString(R.string.new_private_tab); + SHARE_LABEL = res.getString(R.string.share); + FIND_IN_PAGE_LABEL = res.getString(R.string.find_in_page); + DESKTOP_SITE_LABEL = res.getString(R.string.desktop_mode); + PDF_LABEL = res.getString(R.string.save_as_pdf); + DOWNLOADS_LABEL = res.getString(R.string.downloads); + ADDONS_LABEL = res.getString(R.string.addons); + LOGINS_LABEL = res.getString(R.string.logins); + SETTINGS_LABEL = res.getString(R.string.settings); + GUEST_MODE_LABEL = res.getString(R.string.new_guest_session); + TAB_QUEUE_LABEL = res.getString(R.string.pref_tab_queue_title); + TAB_QUEUE_SUMMARY = res.getString(R.string.pref_tab_queue_summary); + + // Android 3.0+ + TOOLS_LABEL = res.getString(R.string.tools); + PAGE_LABEL = res.getString(R.string.page); + + // Android 2.3 and lower only + RELOAD_LABEL = res.getString(R.string.reload); + FORWARD_LABEL = res.getString(R.string.forward); + BOOKMARK_LABEL = res.getString(R.string.bookmark); + + // Bookmark Toast Notification + BOOKMARK_ADDED_LABEL = res.getString(R.string.bookmark_added); + BOOKMARK_REMOVED_LABEL = res.getString(R.string.bookmark_removed); + BOOKMARK_UPDATED_LABEL = res.getString(R.string.bookmark_updated); + BOOKMARK_OPTIONS_LABEL = res.getString(R.string.bookmark_options); + + // Edit Bookmark screen + EDIT_BOOKMARK = res.getString(R.string.bookmark_edit_title); + + // Strings used in doorhanger messages and buttons + GEO_ALLOW = res.getString(R.string.share); + + POPUP_ALLOW = res.getString(R.string.pref_panels_show); + + // Home Settings + PANELS = res.getString(R.string.pref_category_home_panels); + CUSTOMIZE_HOME = res.getString(R.string.pref_category_home); + ENABLED = res.getString(R.string.pref_home_updates_enabled); + HISTORY = res.getString(R.string.home_history_title); + + // Search Settings + SEARCH_TITLE = res.getString(R.string.search); + SEARCH_SUGGESTIONS = res.getString(R.string.pref_search_suggestions); + SEARCH_INSTALLED = res.getString(R.string.pref_category_installed_search_engines); + + // Advanced Settings + ADVANCED = res.getString(R.string.pref_category_advanced); + DONT_SHOW_MENU = res.getString(R.string.pref_char_encoding_off); + SHOW_MENU = res.getString(R.string.pref_char_encoding_on); + DISABLED = res.getString(R.string.pref_plugins_disabled ); + TAP_TO_PLAY = res.getString(R.string.pref_plugins_tap_to_play); + HIDE_TITLE_BAR = res.getString(R.string.pref_scroll_title_bar_summary ); + + // Update Settings + AUTOMATIC_UPDATES = res.getString(R.string.pref_home_updates); + OVER_WIFI_OPTION = res.getString(R.string.pref_update_autodownload_wifi); + DOWNLOAD_UPDATES_AUTO = res.getString(R.string.pref_update_autodownload); + ALWAYS = res.getString(R.string.pref_update_autodownload_enabled); + NEVER = res.getString(R.string.pref_update_autodownload_disabled); + + // Restore Tabs Settings + DONT_RESTORE_TABS = res.getString(R.string.pref_restore_quit); + ALWAYS_RESTORE_TABS = res.getString(R.string.pref_restore_always); + DONT_RESTORE_QUIT = res.getString(R.string.pref_restore_quit); + } + + public static void initialize(Resources res) { + if (instance != null) { + throw new IllegalStateException(StringHelper.class.getSimpleName() + " already Initialized"); + } + instance = new StringHelper(res); + } + + public static StringHelper get() { + if (instance == null) { + throw new IllegalStateException(StringHelper.class.getSimpleName() + " instance is not yet initialized. Use StringHelper.initialize(Resources) first."); + } + return instance; + } + + /** + * Build a URL for loading a Javascript file in the Robocop Javascript + * harness. + * <p> + * We append a random slug to avoid caching: see + * <a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache">https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache</a>. + * + * @param javascriptUrl to load. + * @return URL with harness wrapper. + */ + public String getHarnessUrlForJavascript(String javascriptUrl) { + // We include a slug to make sure we never cache the harness. + return ROBOCOP_JS_HARNESS_URL + + "?slug=" + System.currentTimeMillis() + + "&path=" + javascriptUrl; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java new file mode 100644 index 000000000..da952b5cb --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITest.java @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Assert; +import org.mozilla.gecko.Driver; +import org.mozilla.gecko.tests.components.AboutHomeComponent; +import org.mozilla.gecko.tests.components.AppMenuComponent; +import org.mozilla.gecko.tests.components.BaseComponent; +import org.mozilla.gecko.tests.components.GeckoViewComponent; +import org.mozilla.gecko.tests.components.TabStripComponent; +import org.mozilla.gecko.tests.components.ToolbarComponent; +import org.mozilla.gecko.tests.helpers.HelperInitializer; + +import com.robotium.solo.Solo; + +/** + * A base test class for Robocop (UI-centric) tests. This and the related classes attempt to + * provide a framework to improve upon the issues discovered with the previous BaseTest + * implementation by providing simple test authorship and framework extension, consistency, + * and reliability. + * + * For documentation on writing tests and extending the framework, see + * https://wiki.mozilla.org/Mobile/Fennec/Android/UITest + */ +abstract class UITest extends BaseRobocopTest + implements UITestContext { + + private static final String JUNIT_FAILURE_MSG = "A JUnit method was called. Make sure " + + "you are using AssertionHelper to make assertions. Try `fAssert*(...);`"; + + protected AboutHomeComponent mAboutHome; + protected AppMenuComponent mAppMenu; + protected GeckoViewComponent mGeckoView; + protected TabStripComponent mTabStrip; + protected ToolbarComponent mToolbar; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // Helpers depend on components so initialize them first. + initComponents(); + initHelpers(); + + // Ensure Robocop tests have access to network, and are run with Display powered on. + throwIfHttpGetFails(); + throwIfScreenNotOn(); + } + + private void initComponents() { + mAboutHome = new AboutHomeComponent(this); + mAppMenu = new AppMenuComponent(this); + mGeckoView = new GeckoViewComponent(this); + mTabStrip = new TabStripComponent(this); + mToolbar = new ToolbarComponent(this); + } + + private void initHelpers() { + HelperInitializer.init(this); + } + + @Override + public Solo getSolo() { + return mSolo; + } + + @Override + public Assert getAsserter() { + return mAsserter; + } + + @Override + public Driver getDriver() { + return mDriver; + } + + @Override + public Actions getActions() { + return mActions; + } + + @Override + public StringHelper getStringHelper() { + return mStringHelper; + } + + @Override + public void dumpLog(final String logtag, final String message) { + mAsserter.dumpLog(logtag + ": " + message); + } + + @Override + public void dumpLog(final String logtag, final String message, final Throwable t) { + mAsserter.dumpLog(logtag + ": " + message, t); + } + + @Override + public BaseComponent getComponent(final ComponentType type) { + switch (type) { + case ABOUTHOME: + return mAboutHome; + + case APPMENU: + return mAppMenu; + + case GECKOVIEW: + return mGeckoView; + + case TOOLBAR: + return mToolbar; + + default: + fail("Unknown component type, " + type + "."); + return null; // Should not reach this statement but required by javac. + } + } + + /** + * Returns the test type. By default this returns MOCHITEST, but tests can override this + * method in order to change the type of the test. + */ + @Override + protected Type getTestType() { + return Type.MOCHITEST; + } + + @Override + public String getAbsoluteHostnameUrl(final String url) { + return getAbsoluteUrl(mBaseHostnameUrl, url); + } + + @Override + public String getAbsoluteIpUrl(final String url) { + return getAbsoluteUrl(mBaseIpUrl, url); + } + + private String getAbsoluteUrl(final String baseUrl, final String url) { + return baseUrl + "/" + url.replaceAll("(^/)", ""); + } + + /** + * Throws an Exception. Called from overridden JUnit methods to ensure JUnit assertions + * are not accidentally used over AssertionHelper assertions (the latter of which contains + * additional logging facilities for use in our test harnesses). + */ + private static void junit() { + throw new UnsupportedOperationException(JUNIT_FAILURE_MSG); + } + + // Note: inexplicably, javac does not think we're overriding these methods, + // so we can't use the @Override annotation. + public static void assertEquals(short e, short a) { junit(); } + public static void assertEquals(String m, int e, int a) { junit(); } + public static void assertEquals(String m, short e, short a) { junit(); } + public static void assertEquals(char e, char a) { junit(); } + public static void assertEquals(String m, String e, String a) { junit(); } + public static void assertEquals(int e, int a) { junit(); } + public static void assertEquals(String m, double e, double a, double delta) { junit(); } + public static void assertEquals(String m, long e, long a) { junit(); } + public static void assertEquals(byte e, byte a) { junit(); } + public static void assertEquals(Object e, Object a) { junit(); } + public static void assertEquals(boolean e, boolean a) { junit(); } + public static void assertEquals(String m, float e, float a, float delta) { junit(); } + public static void assertEquals(String m, boolean e, boolean a) { junit(); } + public static void assertEquals(String e, String a) { junit(); } + public static void assertEquals(float e, float a, float delta) { junit(); } + public static void assertEquals(String m, byte e, byte a) { junit(); } + public static void assertEquals(double e, double a, double delta) { junit(); } + public static void assertEquals(String m, char e, char a) { junit(); } + public static void assertEquals(String m, Object e, Object a) { junit(); } + public static void assertEquals(long e, long a) { junit(); } + + public static void assertFalse(String m, boolean c) { junit(); } + public static void assertFalse(boolean c) { junit(); } + + public static void assertNotNull(String m, Object o) { junit(); } + public static void assertNotNull(Object o) { junit(); } + + public static void assertNotSame(Object e, Object a) { junit(); } + public static void assertNotSame(String m, Object e, Object a) { junit(); } + + public static void assertNull(Object o) { junit(); } + public static void assertNull(String m, Object o) { junit(); } + + public static void assertSame(Object e, Object a) { junit(); } + public static void assertSame(String m, Object e, Object a) { junit(); } + + public static void assertTrue(String m, boolean c) { junit(); } + public static void assertTrue(boolean c) { junit(); } + + public static void fail(String m) { junit(); } + public static void fail() { junit(); } + + public static void failNotEquals(String m, Object e, Object a) { junit(); } + public static void failNotSame(String m, Object e, Object a) { junit(); } + public static void failSame(String m) { junit(); } + + public static String format(String m, Object e, Object a) { junit(); return null; } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java new file mode 100644 index 000000000..c825a20a4 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/UITestContext.java @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Assert; +import org.mozilla.gecko.Driver; +import org.mozilla.gecko.tests.components.BaseComponent; + +import android.app.Activity; +import android.app.Instrumentation; + +import com.robotium.solo.Solo; + +/** + * Interface to the global information about a UITest environment. + */ +public interface UITestContext { + + public static enum ComponentType { + ABOUTHOME, + APPMENU, + GECKOVIEW, + TOOLBAR + } + + public Activity getActivity(); + public Solo getSolo(); + public Assert getAsserter(); + public Driver getDriver(); + public Actions getActions(); + public Instrumentation getInstrumentation(); + public StringHelper getStringHelper(); + + public void dumpLog(final String logtag, final String message); + public void dumpLog(final String logtag, final String message, final Throwable t); + + /** + * Returns the absolute version of the given URL using the host's hostname. + */ + public String getAbsoluteHostnameUrl(final String url); + + /** + * Returns the absolute version of the given URL using the host's IP address. + */ + public String getAbsoluteIpUrl(final String url); + + public BaseComponent getComponent(final ComponentType type); +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java new file mode 100644 index 000000000..b12e0d23e --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.components; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.mozilla.gecko.AboutPages; +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.home.HomeConfig.PanelType; +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.tests.helpers.WaitHelper; + +import android.os.Build; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.widget.TextView; + +import com.robotium.solo.Condition; +import com.robotium.solo.Solo; + +/** + * A class representing any interactions that take place on the Awesomescreen. + */ +public class AboutHomeComponent extends BaseComponent { + private static final String LOGTAG = AboutHomeComponent.class.getSimpleName(); + + private static final List<PanelType> PANEL_ORDERING = Arrays.asList( + PanelType.TOP_SITES, + PanelType.BOOKMARKS, + PanelType.COMBINED_HISTORY + ); + + // The percentage of the panel to swipe between 0 and 1. This value was set through + // testing: 0.55f was tested on try and fails on armv6 devices. + private static final float SWIPE_PERCENTAGE = 0.70f; + + public AboutHomeComponent(final UITestContext testContext) { + super(testContext); + } + + private View getHomePagerContainer() { + return mSolo.getView(R.id.home_screen_container); + } + + private ViewPager getHomePagerView() { + return (ViewPager) mSolo.getView(R.id.home_pager); + } + + private View getHomeBannerView() { + if (mSolo.waitForView(R.id.home_banner)) { + return mSolo.getView(R.id.home_banner); + } + return null; + } + + public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) { + assertVisible(); + + final int expectedPanelIndex = PANEL_ORDERING.indexOf(expectedPanel); + fAssertEquals("The current HomePager panel is " + expectedPanel, + expectedPanelIndex, getHomePagerView().getCurrentItem()); + return this; + } + + public AboutHomeComponent assertNotVisible() { + fAssertTrue("The HomePager is not visible", + getHomePagerContainer().getVisibility() != View.VISIBLE || + getHomePagerView().getVisibility() != View.VISIBLE); + return this; + } + + public AboutHomeComponent assertVisible() { + fAssertTrue("The HomePager is visible", + getHomePagerContainer().getVisibility() == View.VISIBLE && + getHomePagerView().getVisibility() == View.VISIBLE); + return this; + } + + public AboutHomeComponent assertBannerNotVisible() { + View banner = getHomeBannerView(); + if (Build.VERSION.SDK_INT >= 11) { + fAssertTrue("The HomeBanner is not visible", + getHomePagerContainer().getVisibility() != View.VISIBLE || + banner == null || + banner.getVisibility() != View.VISIBLE || + banner.getTranslationY() == banner.getHeight()); + } else { + // getTranslationY is not available before api 11. + // This check is a little less specific. + fAssertTrue("The HomeBanner is not visible", + getHomePagerContainer().getVisibility() != View.VISIBLE || + banner == null || + banner.isShown() == false); + } + return this; + } + + public AboutHomeComponent assertBannerVisible() { + fAssertTrue("The HomeBanner is visible", + getHomePagerContainer().getVisibility() == View.VISIBLE && + getHomeBannerView().getVisibility() == View.VISIBLE); + return this; + } + + public AboutHomeComponent assertBannerText(String text) { + assertBannerVisible(); + + final TextView textView = (TextView) getHomeBannerView().findViewById(R.id.text); + fAssertEquals("The correct HomeBanner text is shown", + text, textView.getText().toString()); + return this; + } + + public AboutHomeComponent clickOnBanner() { + assertBannerVisible(); + + mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner."); + mSolo.clickOnView(getHomeBannerView()); + return this; + } + + public AboutHomeComponent dismissBanner() { + assertBannerVisible(); + + mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner close button."); + mSolo.clickOnView(getHomeBannerView().findViewById(R.id.close)); + return this; + } + + public AboutHomeComponent swipeToPanelOnRight() { + mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the right."); + swipeToPanel(Solo.RIGHT); + return this; + } + + public AboutHomeComponent swipeToPanelOnLeft() { + mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the left."); + swipeToPanel(Solo.LEFT); + return this; + } + + private void swipeToPanel(final int panelDirection) { + fAssertTrue("Swiping in a valid direction", + panelDirection == Solo.LEFT || panelDirection == Solo.RIGHT); + assertVisible(); + + final int panelIndex = getHomePagerView().getCurrentItem(); + + mSolo.scrollViewToSide(getHomePagerView(), panelDirection, SWIPE_PERCENTAGE); + + // The panel on the left is a lower index and vice versa. + final int unboundedPanelIndex = panelIndex + (panelDirection == Solo.LEFT ? -1 : 1); + final int maxPanelIndex = PANEL_ORDERING.size() - 1; + final int expectedPanelIndex = Math.min(Math.max(0, unboundedPanelIndex), maxPanelIndex); + + waitForPanelIndex(expectedPanelIndex); + } + + private void waitForPanelIndex(final int expectedIndex) { + final String panelName = PANEL_ORDERING.get(expectedIndex).name(); + + WaitHelper.waitFor("HomePager " + panelName + " panel", new Condition() { + @Override + public boolean isSatisfied() { + return (getHomePagerView().getCurrentItem() == expectedIndex); + } + }); + } + + /** + * Navigate directly to a built-in panel by its panel type. + * <p> + * If the panel type is not part of the active Home Panel configuration, the + * default about:home panel is displayed. If the panel type is not a + * built-in panel, an IllegalArgumentException is thrown. + * + * @param panelType to navigate to. + * @return self, for chaining. + */ + public AboutHomeComponent navigateToBuiltinPanelType(PanelType panelType) throws IllegalArgumentException { + Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(panelType)); + final int expectedPanelIndex = PANEL_ORDERING.indexOf(panelType); + waitForPanelIndex(expectedPanelIndex); + return this; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java new file mode 100644 index 000000000..278cc7564 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AppMenuComponent.java @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.components; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +import java.util.List; +import java.util.concurrent.Callable; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.R; +import org.mozilla.gecko.menu.MenuItemActionBar; +import org.mozilla.gecko.menu.MenuItemDefault; +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.tests.helpers.DeviceHelper; +import org.mozilla.gecko.tests.helpers.RobotiumHelper; +import org.mozilla.gecko.tests.helpers.WaitHelper; + +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; +import android.widget.RelativeLayout; + +import com.robotium.solo.Condition; +import com.robotium.solo.RobotiumUtils; +import com.robotium.solo.Solo; + +/** + * A class representing any interactions that take place on the app menu. + */ +public class AppMenuComponent extends BaseComponent { + private static final int MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS = 7500; + + public enum MenuItem { + FORWARD(R.string.forward), + NEW_TAB(R.string.new_tab), + PAGE(R.string.page), + RELOAD(R.string.reload); + + private final int resourceID; + private String stringResource; + + MenuItem(final int resourceID) { + this.resourceID = resourceID; + } + + public String getString(final Solo solo) { + if (stringResource == null) { + stringResource = solo.getString(resourceID); + } + + return stringResource; + } + }; + + public enum PageMenuItem { + SAVE_AS_PDF(R.string.save_as_pdf); + + private static final MenuItem PARENT_MENU = MenuItem.PAGE; + + private final int resourceID; + private String stringResource; + + PageMenuItem(final int resourceID) { + this.resourceID = resourceID; + } + + public String getString(final Solo solo) { + if (stringResource == null) { + stringResource = solo.getString(resourceID); + } + + return stringResource; + } + }; + + public AppMenuComponent(final UITestContext testContext) { + super(testContext); + } + + public void assertMenuIsOpen() { + fAssertTrue("Menu is open", isMenuOpen()); + } + + public void assertMenuIsNotOpen() { + fAssertFalse("Menu is not open", isMenuOpen()); + } + + public void assertMenuItemIsDisabledAndVisible(PageMenuItem pageMenuItem) { + openAppMenu(); + + // Non-legacy devices have hierarchical menu, check for parent menu item "page". + final View parentMenuItemView = findAppMenuItemView(MenuItem.PAGE.getString(mSolo)); + if (parentMenuItemView.isEnabled()) { + fAssertTrue("The parent 'page' menu item is enabled", parentMenuItemView.isEnabled()); + fAssertEquals("The parent 'page' menu item is visible", View.VISIBLE, + parentMenuItemView.getVisibility()); + + // Parent menu "page" is enabled, open page menu and check for menu item represented by pageMenuItem. + pressMenuItem(MenuItem.PAGE.getString(mSolo)); + + final View pageMenuItemView = findAppMenuItemView(pageMenuItem.getString(mSolo)); + fAssertNotNull("The page menu item is not null", pageMenuItemView); + fAssertFalse("The page menu item is not enabled", pageMenuItemView.isEnabled()); + fAssertEquals("The page menu item is visible", View.VISIBLE, pageMenuItemView.getVisibility()); + } else { + fAssertFalse("The parent 'page' menu item is not enabled", parentMenuItemView.isEnabled()); + fAssertEquals("The parent 'page' menu item is visible", View.VISIBLE, parentMenuItemView.getVisibility()); + } + // Close the App Menu. + mSolo.goBack(); + } + + private View getOverflowMenuButtonView() { + return mSolo.getView(R.id.menu); + } + + /** + * Try to find a MenuItemActionBar/MenuItemDefault with the given text set as contentDescription / text. + * + * When using legacy menus, make sure the menu has been opened to the appropriate level + * (i.e. base menu or "More" menu) to ensure the appropriate menu views are in memory. + * TODO: ^ Maybe we just need to have opened the "More" menu and the current one doesn't matter. + * + * This method is dependent on not having two views with equivalent contentDescription / text. + */ + private View findAppMenuItemView(final String text) { + return WaitHelper.waitFor(String.format("menu item view '%s'", text), new Callable<View>() { + @Override + public View call() throws Exception { + final List<View> views = mSolo.getViews(); + + final List<MenuItemActionBar> menuItemActionBarList = RobotiumUtils.filterViews(MenuItemActionBar.class, views); + for (MenuItemActionBar menuItem : menuItemActionBarList) { + if (TextUtils.equals(menuItem.getContentDescription(), text)) { + return menuItem; + } + } + + final List<MenuItemDefault> menuItemDefaultList = RobotiumUtils.filterViews(MenuItemDefault.class, views); + for (MenuItemDefault menuItem : menuItemDefaultList) { + if (TextUtils.equals(menuItem.getText(), text)) { + return menuItem; + } + } + + // On Android 2.3, menu items may be instances of + // com.android.internal.view.menu.ListMenuItemView, each with a child + // android.widget.RelativeLayout which in turn has a child + // TextView with the appropriate text. + final List<TextView> textViewList = RobotiumUtils.filterViews(TextView.class, views); + for (TextView textView : textViewList) { + if (TextUtils.equals(textView.getText(), text)) { + View relativeLayout = (View) textView.getParent(); + if (relativeLayout instanceof RelativeLayout) { + View listMenuItemView = (View)relativeLayout.getParent(); + return listMenuItemView; + } + } + } + return null; + } + }, MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS); + } + + /** + * Helper function to let Robotium locate and click menu item from legacy Android menu (devices with Android 2.x). + * + * Robotium will also try to open the menu if there are no open dialog. + * + * @param menuItemTitle, The title of menu item to open. + */ + private void pressLegacyMenuItem(final String menuItemTitle) { + mSolo.clickOnMenuItem(menuItemTitle, true); + } + + private void pressMenuItem(final String menuItemTitle) { + // Wait for the menu item view to be enabled. This improves reliability on Android 2.3. + WaitHelper.waitFor(String.format("menu item %s to be enabled", menuItemTitle), new Condition() { + @Override + public boolean isSatisfied() { + View v = findAppMenuItemView(menuItemTitle); + return (v != null) && v.isEnabled(); + } + }); + + final View menuItemView = findAppMenuItemView(menuItemTitle); + fAssertTrue("Menu is open", isMenuOpen(menuItemView)); + + fAssertTrue(String.format("The menu item %s is enabled", menuItemTitle), menuItemView.isEnabled()); + fAssertEquals(String.format("The menu item %s is visible", menuItemTitle), View.VISIBLE, + menuItemView.getVisibility()); + + mSolo.clickOnView(menuItemView); + } + + private void pressSubMenuItem(final String parentMenuItemTitle, final String childMenuItemTitle) { + openAppMenu(); + + pressMenuItem(parentMenuItemTitle); + + // Child menu item is not pressed yet, Click on it. + pressMenuItem(childMenuItemTitle); + } + + public void pressMenuItem(MenuItem menuItem) { + openAppMenu(); + pressMenuItem(menuItem.getString(mSolo)); + } + + public void pressMenuItem(final PageMenuItem pageMenuItem) { + pressSubMenuItem(PageMenuItem.PARENT_MENU.getString(mSolo), pageMenuItem.getString(mSolo)); + } + + private void openAppMenu() { + assertMenuIsNotOpen(); + + // This is a hack needed for tablets where the OverflowMenuButton is always in the GONE state, + // so we press the menu key instead. + if (DeviceHelper.isTablet()) { + mSolo.sendKey(Solo.MENU); + } else { + pressOverflowMenuButton(); + } + + waitForMenuOpen(); + } + + private void pressOverflowMenuButton() { + final View overflowMenuButton = getOverflowMenuButtonView(); + + fAssertTrue("The overflow menu button is enabled", overflowMenuButton.isEnabled()); + fAssertEquals("The overflow menu button is visible", View.VISIBLE, overflowMenuButton.getVisibility()); + + mSolo.clickOnView(overflowMenuButton, true); + } + + /** + * Determines whether the app menu is open by searching for items in the menu. + * + * @return true if app menu is open. + */ + private boolean isMenuOpen() { + // We choose these options because New Tab is near the top of the menu and Page is near the middle/bottom. + // Intermittently, the menu doesn't scroll to top so we can't just use the first item in the list. + return isMenuOpen(MenuItem.NEW_TAB.getString(mSolo)) || isMenuOpen(MenuItem.PAGE.getString(mSolo)); + } + + /** + * Determines whether the app menu is open by searching for the text in menuItemTitle. + * + * @param menuItemTitle, The contentDescription of menu item to search. + * + * @return true if app menu is open. + */ + private boolean isMenuOpen(String menuItemTitle) { + final View menuItemView = findAppMenuItemView(menuItemTitle); + return isMenuOpen(menuItemView) ? true : RobotiumHelper.searchExactText(menuItemTitle, true); + } + + /** + * If a ListMenuItemView with menuItemTitle is visible then the app menu is open . + * + * @param menuItemView, must be a ListMenuItemView with menuItemTitle. + * You must use findAppMenuItemView(menuItemTitle) to obtain it. + * + * @return true if app menu is open. + */ + private boolean isMenuOpen(View menuItemView) { + return (menuItemView != null) && (menuItemView.getVisibility() == View.VISIBLE); + } + + public void waitForMenuOpen() { + WaitHelper.waitFor("menu to open", new Condition() { + @Override + public boolean isSatisfied() { + return isMenuOpen(); + } + }); + } + + public void waitForMenuClose() { + WaitHelper.waitFor("menu to close", new Condition() { + @Override + public boolean isSatisfied() { + return !isMenuOpen(); + } + }); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java new file mode 100644 index 000000000..eadaaa173 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/BaseComponent.java @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.components; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.tests.StringHelper; +import org.mozilla.gecko.tests.UITestContext; + +import android.app.Activity; + +import com.robotium.solo.Solo; + +/** + * A base class for constructing components - an abstraction over small bits of Firefox + * functionality. For example, the Toolbar or the about:home screen could be considered a + * component. Components should not need to know about each others existences and should be + * combined via helpers. Helpers can also handle a series of actions taken on one component + * (e.g. clicking the toolbar, entering a url, and waiting for page load). + */ +public abstract class BaseComponent { + protected final UITestContext mTestContext; + protected final Activity mActivity; + protected final Solo mSolo; + protected final Actions mActions; + protected final StringHelper mStringHelper; + + public BaseComponent(final UITestContext testContext) { + mTestContext = testContext; + mActivity = mTestContext.getActivity(); + mSolo = mTestContext.getSolo(); + mActions = mTestContext.getActions(); + mStringHelper = mTestContext.getStringHelper(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java new file mode 100644 index 000000000..3beab3169 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/GeckoViewComponent.java @@ -0,0 +1,343 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.components; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotSame; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertSame; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.R; +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.tests.helpers.FrameworkHelper; +import org.mozilla.gecko.tests.helpers.WaitHelper; + +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; + +import com.robotium.solo.Condition; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * A class representing any interactions that take place on GeckoView. + */ +public class GeckoViewComponent extends BaseComponent { + + public final TextInput mTextInput; + + public GeckoViewComponent(final UITestContext testContext) { + super(testContext); + mTextInput = new TextInput(); + } + + /** + * Returns the GeckoView. + */ + private View getView() { + // Solo.getView asserts returning a valid View + return mSolo.getView(R.id.layer_view); + } + + private void setContext(final Context newContext) { + final View geckoView = getView(); + // Switch to a no-InputMethodManager context to avoid interference + mTestContext.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + FrameworkHelper.setViewContext(geckoView, newContext); + } + }); + } + + public static abstract class InputConnectionTest { + protected Handler inputConnectionHandler; + + /** + * Processes pending events on the input connection thread before returning. + * Must be called on the input connection thread during a test. + */ + protected void processInputConnectionEvents() { + fAssertSame("Should be called on input connection thread", + Looper.myLooper(), inputConnectionHandler.getLooper()); + + // Adapted from GeckoThread.pumpMessageLoop. + MessageQueue queue = Looper.myQueue(); + queue.addIdleHandler(new MessageQueue.IdleHandler() { + @Override + public boolean queueIdle() { + final Message msg = Message.obtain(inputConnectionHandler); + msg.obj = inputConnectionHandler; + inputConnectionHandler.sendMessageAtFrontOfQueue(msg); + return false; // Remove this idle handler. + } + }); + + final Method getNextMessage; + try { + getNextMessage = queue.getClass().getDeclaredMethod("next"); + } catch (final NoSuchMethodException e) { + throw new UnsupportedOperationException(e); + } + getNextMessage.setAccessible(true); + + while (true) { + final Message msg; + try { + msg = (Message) getNextMessage.invoke(queue); + } catch (final IllegalAccessException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + if (msg.obj == inputConnectionHandler && + msg.getTarget() == inputConnectionHandler) { + // Our idle signal + break; + } else if (msg.getTarget() == null) { + Looper.myLooper().quit(); + break; + } + msg.getTarget().dispatchMessage(msg); + } + } + + /** + * Processes pending events on the Gecko thread before returning. + * Must be called on the input connection thread during a test. + */ + protected void processGeckoEvents() { + fAssertSame("Should be called on input connection thread", + Looper.myLooper(), inputConnectionHandler.getLooper()); + + GeckoThread.waitOnGecko(); + } + + private static ExtractedText getExtractedText(final InputConnection ic) { + final ExtractedTextRequest req = new ExtractedTextRequest(); + return ic.getExtractedText(req, 0); + } + + protected String getText(final InputConnection ic) { + return getExtractedText(ic).text.toString(); + } + + private static void assertText(final String message, + final String expected, + final String actual) { + // In an HTML editor, Gecko may insert an additional element that show up as a + // return character at the end. Deal with that here. + int end = actual.length(); + if (end > 0 && actual.charAt(end - 1) == '\n') { + end--; + } + fAssertEquals(message, expected, actual.substring(0, end)); + } + + protected void assertText(final String message, + final InputConnection ic, + final String text) { + processGeckoEvents(); + processInputConnectionEvents(); + + assertText(message, text, getText(ic)); + } + + protected void assertSelection(final String message, + final InputConnection ic, + final int start, + final int end) { + processGeckoEvents(); + processInputConnectionEvents(); + + final ExtractedText extract = getExtractedText(ic); + fAssertEquals(message, start, extract.selectionStart); + fAssertEquals(message, end, extract.selectionEnd); + } + + protected void assertSelectionAt(final String message, + final InputConnection ic, + final int value) { + assertSelection(message, ic, value, value); + } + + protected void assertTextAndSelection(final String message, + final InputConnection ic, + final String text, + final int start, + final int end) { + processGeckoEvents(); + processInputConnectionEvents(); + + final ExtractedText extract = getExtractedText(ic); + assertText(message, text, extract.text.toString()); + fAssertEquals(message, start, extract.selectionStart); + fAssertEquals(message, end, extract.selectionEnd); + } + + protected void assertTextAndSelectionAt(final String message, + final InputConnection ic, + final String text, + final int selection) { + assertTextAndSelection(message, ic, text, selection, selection); + } + + public abstract void test(InputConnection ic, EditorInfo info); + } + + public class TextInput { + private TextInput() { + } + + private InputMethodManager getInputMethodManager() { + final InputMethodManager imm = (InputMethodManager) + mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); + fAssertNotNull("Must have an InputMethodManager", imm); + return imm; + } + + /** + * Returns whether text input is being directed to the GeckoView. + */ + private boolean isActive() { + return getInputMethodManager().isActive(getView()); + } + + public TextInput assertActive() { + fAssertTrue("Current view should be the active input view", isActive()); + return this; + } + + public TextInput waitForActive() { + WaitHelper.waitFor("current view to become the active input view", new Condition() { + @Override + public boolean isSatisfied() { + return isActive(); + } + }); + return this; + } + + /** + * Returns whether an InputConnection is available. + * An InputConnection is available when text input is being directed to the + * GeckoView, and a text field (input, textarea, contentEditable, etc.) is + * currently focused inside the GeckoView. + */ + private boolean hasInputConnection() { + final InputMethodManager imm = getInputMethodManager(); + return imm.isActive(getView()) && imm.isAcceptingText(); + } + + public TextInput assertInputConnection() { + fAssertTrue("Current view should have an active InputConnection", hasInputConnection()); + return this; + } + + public TextInput waitForInputConnection() { + WaitHelper.waitFor("current view to have an active InputConnection", new Condition() { + @Override + public boolean isSatisfied() { + return hasInputConnection(); + } + }); + return this; + } + + /** + * Starts an InputConnectionTest. An InputConnectionTest must run on the + * InputConnection thread which may or may not be the main UI thread. Also, + * during an InputConnectionTest, the system InputMethodManager service must + * be temporarily disabled to prevent the system IME from interfering with our + * tests. We disable the service by override the GeckoView's context with one + * that returns a null InputMethodManager service. + * + * @param test Test to run + */ + public TextInput testInputConnection(final InputConnectionTest test) { + + fAssertNotNull("Test must not be null", test); + assertInputConnection(); + + // GeckoInputConnection can run on another thread than the main thread, + // so we need to be testing it on that same thread it's running on + final View geckoView = getView(); + final Handler inputConnectionHandler = geckoView.getHandler(); + final Context oldGeckoViewContext = FrameworkHelper.getViewContext(geckoView); + + setContext(new ContextWrapper(oldGeckoViewContext) { + @Override + public Object getSystemService(String name) { + if (Context.INPUT_METHOD_SERVICE.equals(name)) { + return null; + } + return super.getSystemService(name); + } + }); + + (new InputConnectionTestRunner(test, inputConnectionHandler)).launch(); + + setContext(oldGeckoViewContext); + return this; + } + + private class InputConnectionTestRunner implements Runnable { + private final InputConnectionTest mTest; + private boolean mDone; + + public InputConnectionTestRunner(final InputConnectionTest test, + final Handler handler) { + test.inputConnectionHandler = handler; + mTest = test; + } + + public synchronized void launch() { + // Below, we are blocking the instrumentation thread to wait on the + // InputConnection thread. Therefore, the InputConnection thread must not be + // the same as the instrumentation thread to avoid a deadlock. This should + // always be the case and we perform a sanity check to make sure. + fAssertNotSame("InputConnection should not be running on instrumentation thread", + Looper.myLooper(), mTest.inputConnectionHandler.getLooper()); + + mDone = false; + mTest.inputConnectionHandler.post(this); + do { + try { + wait(); + } catch (InterruptedException e) { + // Ignore interrupts + } + } while (!mDone); + } + + @Override + public void run() { + final EditorInfo info = new EditorInfo(); + final InputConnection ic = getView().onCreateInputConnection(info); + fAssertNotNull("Must have an InputConnection", ic); + // Restore the IC to a clean state + ic.clearMetaKeyStates(-1); + ic.finishComposingText(); + mTest.test(ic, info); + synchronized (this) { + // Test finished; return from launch(). + mDone = true; + notify(); + } + } + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java new file mode 100644 index 000000000..e8a90b351 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/TabStripComponent.java @@ -0,0 +1,56 @@ +package org.mozilla.gecko.tests.components; + +import android.view.View; + +import com.robotium.solo.Condition; + +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.tests.helpers.DeviceHelper; +import org.mozilla.gecko.tests.helpers.WaitHelper; +import org.mozilla.gecko.widget.TwoWayView; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; + +/** + * A class representing any interactions that take place on the tablet tab strip. + */ +public class TabStripComponent extends BaseComponent { + // Using a text id because the layout and therefore the id might be stripped from the (non-tablet) build + private static final String TAB_STRIP_ID = "tab_strip"; + + public TabStripComponent(final UITestContext testContext) { + super(testContext); + } + + public void switchToTab(int index) { + // The tab strip is only available on tablets + DeviceHelper.assertIsTablet(); + + View tabView = waitForTabView(index); + fAssertNotNull(String.format("Tab at index %d is not null", index), tabView); + + mSolo.clickOnView(tabView); + } + + private View waitForTabView(final int index) { + final TwoWayView tabStrip = getTabStripView(); + final View[] tabView = new View[1]; + + WaitHelper.waitFor(String.format("Tab at index %d to be visible", index), new Condition() { + @Override + public boolean isSatisfied() { + return (tabView[0] = tabStrip.getChildAt(index)) != null; + } + }); + + return tabView[0]; + } + + private TwoWayView getTabStripView() { + TwoWayView tabStrip = (TwoWayView) mSolo.getView("tab_strip"); + + fAssertNotNull("Tab strip is not null", tabStrip); + + return tabStrip; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java new file mode 100644 index 000000000..25101a395 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/ToolbarComponent.java @@ -0,0 +1,326 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.components; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.tests.helpers.DeviceHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; +import org.mozilla.gecko.tests.helpers.WaitHelper; +import org.mozilla.gecko.toolbar.PageActionLayout; + +import android.view.View; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.robotium.solo.Condition; +import com.robotium.solo.Solo; + +/** + * A class representing any interactions that take place on the Toolbar. + */ +public class ToolbarComponent extends BaseComponent { + + private static final String URL_HTTP_PREFIX = "http://"; + + // We are waiting up to 30 seconds instead of the default waiting time because reader mode + // parsing can take quite some time on slower devices (Bug 1142699) + private static final int READER_MODE_WAIT_MS = 30000; + + public ToolbarComponent(final UITestContext testContext) { + super(testContext); + } + + public ToolbarComponent assertIsEditing() { + fAssertTrue("The toolbar is in the editing state", isEditing()); + return this; + } + + public ToolbarComponent assertIsNotEditing() { + fAssertFalse("The toolbar is not in the editing state", isEditing()); + return this; + } + + public ToolbarComponent assertTitle(final String url) { + fAssertNotNull("The url argument is not null", url); + + final String expected; + final String absoluteURL = NavigationHelper.adjustUrl(url); + + if (mStringHelper.ABOUT_HOME_URL.equals(absoluteURL)) { + expected = mStringHelper.ABOUT_HOME_TITLE; + } else if (absoluteURL.startsWith(URL_HTTP_PREFIX)) { + expected = absoluteURL.substring(URL_HTTP_PREFIX.length()); + } else { + expected = absoluteURL; + } + + // Since we only display a shortened "base domain" (See bug 1236431) we use the content + // description to obtain the full URL. + fAssertEquals("The Toolbar title is " + expected, expected, getUrlFromContentDescription()); + return this; + } + + public ToolbarComponent assertUrl(final String expected) { + assertIsEditing(); + fAssertEquals("The Toolbar url is " + expected, expected, getUrlEditText().getText()); + return this; + } + + public ToolbarComponent assertIsUrlEditTextSelected() { + fAssertTrue("The edit text is selected", isUrlEditTextSelected()); + return this; + } + + public ToolbarComponent assertIsUrlEditTextNotSelected() { + fAssertFalse("The edit text is not selected", isUrlEditTextSelected()); + return this; + } + + public ToolbarComponent assertBackButtonIsNotEnabled() { + fAssertFalse("The back button is not enabled", isBackButtonEnabled()); + return this; + } + + /** + * Returns the root View for the browser toolbar. + */ + private View getToolbarView() { + mSolo.waitForView(R.id.browser_toolbar); + return mSolo.getView(R.id.browser_toolbar); + } + + private EditText getUrlEditText() { + return (EditText) getToolbarView().findViewById(R.id.url_edit_text); + } + + private View getUrlDisplayLayout() { + return getToolbarView().findViewById(R.id.display_layout); + } + + private TextView getUrlTitleText() { + return (TextView) getToolbarView().findViewById(R.id.url_bar_title); + } + + private ImageButton getBackButton() { + DeviceHelper.assertIsTablet(); + return (ImageButton) getToolbarView().findViewById(R.id.back); + } + + private ImageButton getForwardButton() { + DeviceHelper.assertIsTablet(); + return (ImageButton) getToolbarView().findViewById(R.id.forward); + } + + private ImageButton getReloadButton() { + DeviceHelper.assertIsTablet(); + return (ImageButton) getToolbarView().findViewById(R.id.reload); + } + + private PageActionLayout getPageActionLayout() { + return (PageActionLayout) getToolbarView().findViewById(R.id.page_action_layout); + } + + private ImageButton getReaderModeButton() { + final PageActionLayout pageActionLayout = getPageActionLayout(); + final int count = pageActionLayout.getChildCount(); + + for (int i = 0; i < count; i++) { + final View view = pageActionLayout.getChildAt(i); + if (mStringHelper.CONTENT_DESCRIPTION_READER_MODE_BUTTON.equals(view.getContentDescription())) { + return (ImageButton) view; + } + } + + return null; + } + + /** + * Returns the View for the edit cancel button in the browser toolbar. + */ + private View getEditCancelButton() { + return getToolbarView().findViewById(R.id.edit_cancel); + } + + private String getUrlFromContentDescription() { + assertIsNotEditing(); + + final CharSequence contentDescription = getUrlDisplayLayout().getContentDescription(); + if (contentDescription == null) { + return ""; + } else { + return contentDescription.toString(); + } + } + + /** + * Returns the title of the page. Note that this makes no assertions to Toolbar state and + * may return a value that may never be visible to the user. Callers likely want to use + * {@link assertTitle} instead. + */ + public String getPotentiallyInconsistentTitle() { + return getTitleHelper(false); + } + + private String getTitleHelper(final boolean shouldAssertNotEditing) { + if (shouldAssertNotEditing) { + assertIsNotEditing(); + } + + return getUrlTitleText().getText().toString(); + } + + private boolean isEditing() { + return getUrlDisplayLayout().getVisibility() != View.VISIBLE && + getUrlEditText().getVisibility() == View.VISIBLE; + } + + public ToolbarComponent enterEditingMode() { + assertIsNotEditing(); + + mSolo.clickOnView(getUrlTitleText(), true); + + waitForEditing(); + WaitHelper.waitFor("UrlEditText to be input method target", new Condition() { + @Override + public boolean isSatisfied() { + return getUrlEditText().isInputMethodTarget(); + } + }); + + return this; + } + + public ToolbarComponent commitEditingMode() { + assertIsEditing(); + + WaitHelper.waitForPageLoad(new Runnable() { + @Override + public void run() { + mSolo.sendKey(Solo.ENTER); + } + }); + waitForNotEditing(); + + return this; + } + + public ToolbarComponent dismissEditingMode() { + assertIsEditing(); + + if (DeviceHelper.isTablet()) { + final EditText urlEditText = getUrlEditText(); + if (urlEditText.isFocused()) { + mSolo.goBack(); + } + mSolo.goBack(); + } else { + mSolo.clickOnView(getEditCancelButton()); + } + + waitForNotEditing(); + + return this; + } + + public ToolbarComponent enterUrl(final String url) { + fAssertNotNull("url is not null", url); + + assertIsEditing(); + + final EditText urlEditText = getUrlEditText(); + fAssertTrue("The UrlEditText is the input method target", + urlEditText.isInputMethodTarget()); + + mSolo.clearEditText(urlEditText); + mSolo.typeText(urlEditText, url); + + return this; + } + + public ToolbarComponent pressBackButton() { + final ImageButton backButton = getBackButton(); + return pressButton(backButton, "back"); + } + + public ToolbarComponent pressForwardButton() { + final ImageButton forwardButton = getForwardButton(); + return pressButton(forwardButton, "forward"); + } + + public ToolbarComponent pressReloadButton() { + final ImageButton reloadButton = getReloadButton(); + return pressButton(reloadButton, "reload"); + } + + public ToolbarComponent pressReaderModeButton() { + final ImageButton readerModeButton = waitForReaderModeButton(); + pressButton(readerModeButton, "reader mode"); + + return this; + } + + private ToolbarComponent pressButton(final View view, final String buttonName) { + fAssertNotNull("The " + buttonName + " button View is not null", view); + fAssertTrue("The " + buttonName + " button is enabled", view.isEnabled()); + fAssertEquals("The " + buttonName + " button is visible", + View.VISIBLE, view.getVisibility()); + assertIsNotEditing(); + + WaitHelper.waitForPageLoad(new Runnable() { + @Override + public void run() { + mSolo.clickOnView(view); + } + }); + + return this; + } + + private void waitForEditing() { + WaitHelper.waitFor("Toolbar to enter editing mode", new Condition() { + @Override + public boolean isSatisfied() { + return isEditing(); + } + }); + } + + private void waitForNotEditing() { + WaitHelper.waitFor("Toolbar to exit editing mode", new Condition() { + @Override + public boolean isSatisfied() { + return !isEditing(); + } + }); + } + + private ImageButton waitForReaderModeButton() { + final ImageButton[] readerModeButton = new ImageButton[1]; + + WaitHelper.waitFor("the Reader mode button to be visible", new Condition() { + @Override + public boolean isSatisfied() { + return (readerModeButton[0] = getReaderModeButton()) != null; + } + }, READER_MODE_WAIT_MS); + + return readerModeButton[0]; + } + + private boolean isUrlEditTextSelected() { + return getUrlEditText().isSelected(); + } + + private boolean isBackButtonEnabled() { + return getBackButton().isEnabled(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java new file mode 100644 index 000000000..894d134d1 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/AssertionHelper.java @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import java.util.Arrays; + +import org.mozilla.gecko.Assert; +import org.mozilla.gecko.tests.UITestContext; + +/** + * Provides assertions in a JUnit-like API that wraps the robocop Assert interface. + */ +public final class AssertionHelper { + // Assert.ok has a "diag" ("diagnostic") parameter that has no useful purpose. + private static final String DIAG_STRING = ""; + + private static Assert sAsserter; + + private AssertionHelper() { /* To disallow instantiation. */ } + + protected static void init(final UITestContext context) { + sAsserter = context.getAsserter(); + } + + public static void fAssertArrayEquals(final String message, final byte[] expecteds, final byte[] actuals) { + sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING); + } + + public static void fAssertArrayEquals(final String message, final char[] expecteds, final char[] actuals) { + sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING); + } + + public static void fAssertArrayEquals(final String message, final short[] expecteds, final short[] actuals) { + sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING); + } + + public static void fAssertArrayEquals(final String message, final int[] expecteds, final int[] actuals) { + sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING); + } + + public static void fAssertArrayEquals(final String message, final long[] expecteds, final long[] actuals) { + sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING); + } + + public static void fAssertArrayEquals(final String message, final Object[] expecteds, final Object[] actuals) { + sAsserter.ok(Arrays.equals(expecteds, actuals), message, DIAG_STRING); + } + + public static void fAssertEquals(final String message, final double expected, final double actual, final double delta) { + if (Double.compare(expected, actual) != 0) { + sAsserter.ok(Math.abs(expected - actual) <= delta, message, DIAG_STRING); + } + } + + public static void fAssertEquals(final String message, final long expected, final long actual) { + sAsserter.is(actual, expected, message); + } + + public static void fAssertEquals(final String message, final Object expected, final Object actual) { + sAsserter.is(actual, expected, message); + } + + public static void fAssertNotEquals(final String message, final double unexpected, final double actual, final double delta) { + sAsserter.ok(Math.abs(unexpected - actual) > delta, message, DIAG_STRING); + } + + public static void fAssertNotEquals(final String message, final long unexpected, final long actual) { + sAsserter.isnot(actual, unexpected, message); + } + + public static void fAssertNotEquals(final String message, final Object unexpected, final Object actual) { + sAsserter.isnot(actual, unexpected, message); + } + + public static void fAssertFalse(final String message, final boolean actual) { + sAsserter.ok(!actual, message, DIAG_STRING); + } + + public static void fAssertNotNull(final String message, final Object actual) { + sAsserter.isnot(actual, null, message); + } + + public static void fAssertNotSame(final String message, final Object unexpected, final Object actual) { + sAsserter.ok(unexpected != actual, message, DIAG_STRING); + } + + public static void fAssertNull(final String message, final Object actual) { + sAsserter.is(actual, null, message); + } + + public static void fAssertSame(final String message, final Object expected, final Object actual) { + sAsserter.ok(expected == actual, message, DIAG_STRING); + } + + public static void fAssertTrue(final String message, final boolean actual) { + sAsserter.ok(actual, message, DIAG_STRING); + } + + public static void fAssertIsPixel(final String message, final int actual, final int r, final int g, final int b) { + sAsserter.ispixel(actual, r, g, b, message); + } + + public static void fAssertIsNotPixel(final String message, final int actual, final int r, final int g, final int b) { + sAsserter.isnotpixel(actual, r, g, b, message); + } + + public static void fFail(final String message) { + sAsserter.ok(false, message, DIAG_STRING); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java new file mode 100644 index 000000000..476bd34dd --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/DeviceHelper.java @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.tests.UITestContext; + +import android.app.Activity; +import android.os.Build; +import android.util.DisplayMetrics; + +import com.robotium.solo.Solo; + +/** + * Provides general hardware (ex: configuration) and software (ex: version) information + * about the current test device and allows changing its configuration. + */ +public final class DeviceHelper { + public enum Type { + PHONE, + TABLET + } + + public enum AndroidVersion { + v2x, + v3x, + v4x + } + + private static Activity sActivity; + private static Solo sSolo; + + private static Type sDeviceType; + private static AndroidVersion sAndroidVersion; + + private static int sScreenHeight; + private static int sScreenWidth; + + private DeviceHelper() { /* To disallow instantiation. */ } + + public static void assertIsTablet() { + fAssertTrue("The device is a tablet", isTablet()); + } + + protected static void init(final UITestContext context) { + sActivity = context.getActivity(); + sSolo = context.getSolo(); + + setAndroidVersion(); + setScreenDimensions(); + setDeviceType(); + } + + private static void setAndroidVersion() { + int sdk = Build.VERSION.SDK_INT; + if (sdk < Build.VERSION_CODES.HONEYCOMB) { + sAndroidVersion = AndroidVersion.v2x; + } else if (sdk > Build.VERSION_CODES.HONEYCOMB_MR2) { + sAndroidVersion = AndroidVersion.v4x; + } else { + sAndroidVersion = AndroidVersion.v3x; + } + } + + private static void setScreenDimensions() { + final DisplayMetrics dm = new DisplayMetrics(); + sActivity.getWindowManager().getDefaultDisplay().getMetrics(dm); + + sScreenHeight = dm.heightPixels; + sScreenWidth = dm.widthPixels; + } + + private static void setDeviceType() { + sDeviceType = (GeckoAppShell.isTablet() ? Type.TABLET : Type.PHONE); + } + + public static int getScreenHeight() { + return sScreenHeight; + } + + public static int getScreenWidth() { + return sScreenWidth; + } + + public static AndroidVersion getAndroidVersion() { + return sAndroidVersion; + } + + public static boolean isPhone() { + return (sDeviceType == Type.PHONE); + } + + public static boolean isTablet() { + return (sDeviceType == Type.TABLET); + } + + public static void setLandscapeRotation() { + sSolo.setActivityOrientation(Solo.LANDSCAPE); + } + + public static void setPortraitOrientation() { + sSolo.setActivityOrientation(Solo.LANDSCAPE); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java new file mode 100644 index 000000000..d3c4d6390 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/FrameworkHelper.java @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail; + +import java.lang.reflect.Field; + +import android.content.Context; +import android.view.View; + +/** + * Provides helper functions for accessing Android framework features + * + * This class uses reflection to access framework functionalities that are + * unavailable through the regular Android API. Using reflection in this + * case is okay because it does not touch Gecko classes that go through + * ProGuard. + */ +public final class FrameworkHelper { + + private FrameworkHelper() { /* To disallow instantiation. */ } + + private static Field getClassField(final Class<?> clazz, final String fieldName) + throws NoSuchFieldException { + Class<?> cls = clazz; + do { + try { + return cls.getDeclaredField(fieldName); + } catch (final Exception e) { + // NoSuchFieldException is a documented exception of getDeclaredField + // and is frequently observed here. No other exceptions are documented + // for getDeclaredField. However, on Android 2.3, NoSuchMethodException + // is also observed, when called on some classes. This appears to be + // an Android bug reportedly fixed in Honeycomb. Since NoSuchMethodException + // is not declared, it cannot be caught, so we catch all Exceptions. + cls = cls.getSuperclass(); + } + } while (cls != null); + // We tried getDeclaredField before; now try getField instead. + // getField behaves differently in that getField traverses the inheritance + // list, but it only works on public fields. While getField won't get us + // anything new, it makes code cleaner by throwing an exception for us. + return clazz.getField(fieldName); + } + + private static Object getField(final Object obj, final String fieldName) { + try { + final Field field = getClassField(obj.getClass(), fieldName); + final boolean accessible = field.isAccessible(); + field.setAccessible(true); + final Object ret = field.get(obj); + field.setAccessible(accessible); + return ret; + } catch (final NoSuchFieldException e) { + // We expect a valid field name; if it's not valid, + // the caller is doing something wrong and should be fixed. + fFail("Argument field should be a valid field name: " + e.toString()); + } catch (final IllegalAccessException e) { + // This should not happen. If it does, setAccessible above is not working. + fFail("Field should be accessible: " + e.toString()); + } + throw new IllegalStateException("Should not continue from previous failures"); + } + + private static void setField(final Object obj, final String fieldName, final Object value) { + try { + final Field field = getClassField(obj.getClass(), fieldName); + final boolean accessible = field.isAccessible(); + field.setAccessible(true); + field.set(obj, value); + field.setAccessible(accessible); + return; + } catch (final NoSuchFieldException e) { + // We expect a valid field name; if it's not valid, + // the caller is doing something wrong and should be fixed. + fFail("Argument field should be a valid field name: " + e.toString()); + } catch (final IllegalAccessException e) { + // This should not happen. If it does, setAccessible above is not working. + fFail("Field should be accessible: " + e.toString()); + } + throw new IllegalStateException("Cannot continue from previous failures"); + } + + public static Context getViewContext(final View v) { + return (Context) getField(v, "mContext"); + } + + public static void setViewContext(final View v, final Context c) { + setField(v, "mContext", c); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java new file mode 100644 index 000000000..b8d1ef0ce --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoClickHelper.java @@ -0,0 +1,50 @@ +package org.mozilla.gecko.tests.helpers; + +import android.app.Activity; +import android.util.DisplayMetrics; + +import com.robotium.solo.Solo; + +import org.mozilla.gecko.Driver; +import org.mozilla.gecko.tests.StringHelper; +import org.mozilla.gecko.tests.UITestContext; + +/** + * Provides helper functions for clicking elements rendered by the Gecko engine. + */ +public class GeckoClickHelper { + private static Solo sSolo; + private static Activity sActivity; + private static Driver sDriver; + + protected static void init(final UITestContext context) { + sSolo = context.getSolo(); + sActivity = context.getActivity(); + sDriver = context.getDriver(); + } + + private GeckoClickHelper() { /* To disallow instantiation. */ } + + /** + * Long press the link and select "Open Link in New Tab" from the context menu. + * + * The link should be positioned at the top of the page, at least 60px high and + * aligned to the middle. + */ + public static void openCentralizedLinkInNewTab() { + openLinkContextMenu(); + + // Click on "Open Link in New Tab" + sSolo.clickOnText(StringHelper.get().CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]); + } + + private static void openLinkContextMenu() { + DisplayMetrics dm = new DisplayMetrics(); + sActivity.getWindowManager().getDefaultDisplay().getMetrics(dm); + + sSolo.clickLongOnScreen( + sDriver.getGeckoLeft() + sDriver.getGeckoWidth() / 2, + sDriver.getGeckoTop() + 30 * dm.density + ); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java new file mode 100644 index 000000000..cd75b7255 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Actions.EventExpecter; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.tests.UITestContext; + +import android.app.Activity; + +/** + * Provides helper functions for accessing the underlying Gecko engine. + */ +public final class GeckoHelper { + private static Activity sActivity; + private static Actions sActions; + + private GeckoHelper() { /* To disallow instantiation. */ } + + protected static void init(final UITestContext context) { + sActivity = context.getActivity(); + sActions = context.getActions(); + } + + public static void blockForReady() { + blockForEvent("Gecko:Ready"); + } + + /** + * Blocks for the "Gecko:DelayedStartup" event, which occurs after "Gecko:Ready" and the + * first page load. + */ + public static void blockForDelayedStartup() { + blockForEvent("Gecko:DelayedStartup"); + } + + private static void blockForEvent(final String eventName) { + final EventExpecter eventExpecter = sActions.expectGeckoEvent(eventName); + + if (!GeckoThread.isRunning()) { + eventExpecter.blockForEvent(); + } + + eventExpecter.unregisterListener(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java new file mode 100644 index 000000000..229dc1062 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/HelperInitializer.java @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import org.mozilla.gecko.tests.UITestContext; + +/** + * AssertionHelper is statically imported in many places. Thus we want to hide + * its init method outside of this package. We initialize the remaining helper + * classes from here so that all the init methods are package protected. + */ +public final class HelperInitializer { + + private HelperInitializer() { /* To disallow instantiation. */ } + + public static void init(final UITestContext context) { + // Other helpers make assertions so init AssertionHelper first. + AssertionHelper.init(context); + + DeviceHelper.init(context); + GeckoClickHelper.init(context); + GeckoHelper.init(context); + JavascriptBridge.init(context); + NavigationHelper.init(context); + RobotiumHelper.init(context); + WaitHelper.init(context); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java new file mode 100644 index 000000000..1b0ece1cd --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptBridge.java @@ -0,0 +1,394 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import junit.framework.AssertionFailedError; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Actions.EventExpecter; +import org.mozilla.gecko.Assert; +import org.mozilla.gecko.tests.UITestContext; + +/** + * Javascript bridge allows calls to and from JavaScript. + * + * To establish communication, create an instance of JavascriptBridge in Java and pass in + * an object that will receive calls from JavaScript. For example: + * + * {@code final JavascriptBridge js = new JavascriptBridge(javaObj);} + * + * Next, create an instance of JavaBridge in JavaScript and pass in another object + * that will receive calls from Java. For example: + * + * {@code let java = new JavaBridge(jsObj);} + * + * Once a link is established, calls can be made using the methods syncCall and asyncCall. + * syncCall waits for the call to finish before returning. For example: + * + * {@code js.syncCall("abc", 1, 2, 3);} will synchronously call the JavaScript method + * jsObj.abc and pass in arguments 1, 2, and 3. + * + * {@code java.asyncCall("def", 4, 5, 6);} will asynchronously call the Java method + * javaObj.def and pass in arguments 4, 5, and 6. + * + * Supported argument types include int, double, boolean, String, and JSONObject. Note + * that only implicit conversion is done, meaning if a floating point argument is passed + * from JavaScript to Java, the call will fail if the Java method has an int argument. + * + * Because JavascriptBridge and JavaBridge use one underlying communication channel, + * creating multiple instances of them will not create independent links. + * + * Note also that because Robocop tests finish as soon as the Java test method returns, + * the last call to JavaScript from Java must be a synchronous call. Otherwise, the test + * will finish before the JavaScript method is run. Calls to Java from JavaScript do not + * have this requirement. Because of these considerations, calls from Java to JavaScript + * are usually synchronous and calls from JavaScript to Java are usually asynchronous. + * See testJavascriptBridge.java for examples. + */ +public final class JavascriptBridge { + + private static enum MessageStatus { + QUEUE_EMPTY, // Did not process a message; queue was empty. + PROCESSED, // A message other than sync was processed. + REPLIED, // A sync message was processed. + SAVED, // An async message was saved; see processMessage(). + }; + + @SuppressWarnings("serial") + public static class CallException extends RuntimeException { + public CallException() { + super(); + } + + public CallException(final String msg) { + super(msg); + } + + public CallException(final String msg, final Throwable e) { + super(msg, e); + } + + public CallException(final Throwable e) { + super(e); + } + } + + public static final String EVENT_TYPE = "Robocop:JS"; + + private static Actions sActions; + private static Assert sAsserter; + + // Target of JS-to-Java calls + private final Object mTarget; + // List of public methods in subclass + private final Method[] mMethods; + // Parser for handling xpcshell assertions + private final JavascriptMessageParser mLogParser; + // Expecter of our internal Robocop event + private final EventExpecter mExpecter; + // Saved async message; see processMessage() for its purpose. + private JSONObject mSavedAsyncMessage; + // Number of levels in the synchronous call stack + private int mCallStackDepth; + // If JavaBridge has been loaded + private boolean mJavaBridgeLoaded; + + /* package */ static void init(final UITestContext context) { + sActions = context.getActions(); + sAsserter = context.getAsserter(); + } + + public JavascriptBridge(final Object target) { + mTarget = target; + mMethods = target.getClass().getMethods(); + mExpecter = sActions.expectGeckoEvent(EVENT_TYPE); + // The JS here is unrelated to a test harness, so we + // have our message parser end on assertion failure. + mLogParser = new JavascriptMessageParser(sAsserter, true); + } + + /** + * Synchronously calls a method in Javascript. + * + * @param method Name of the method to call + * @param args Arguments to pass to the Javascript method; must be a list of + * values allowed by JSONObject. + */ + public void syncCall(final String method, final Object... args) { + mCallStackDepth++; + + sendMessage("sync-call", method, args); + try { + while (processPendingMessage() != MessageStatus.REPLIED) { + } + } catch (final AssertionFailedError e) { + // Most likely an event expecter time out + throw new CallException("Cannot call " + method, e); + } + + // If syncCall was called reentrantly from processPendingMessage(), mCallStackDepth + // will be greater than 1 here. In that case we don't have to wait for pending calls + // because the outermost syncCall will do it for us. + if (mCallStackDepth == 1) { + // We want to wait for all asynchronous calls to finish, + // because the test may end immediately after this method returns. + finishPendingCalls(); + } + mCallStackDepth--; + } + + /** + * Asynchronously calls a method in Javascript. + * + * @param method Name of the method to call + * @param args Arguments to pass to the Javascript method; must be a list of + * values allowed by JSONObject. + */ + public void asyncCall(final String method, final Object... args) { + sendMessage("async-call", method, args); + } + + /** + * Disconnect the bridge. + */ + public void disconnect() { + mExpecter.unregisterListener(); + } + + /** + * Process a new message; wait for new message if necessary. + * + * @return MessageStatus value to indicate result of processing the message + */ + private MessageStatus processPendingMessage() { + // We're on the test thread. + // We clear mSavedAsyncMessage in maybeProcessPendingMessage() but not here, + // because we always have a new message for processing here, so we never + // get a chance to clear mSavedAsyncMessage. + try { + final String message = mExpecter.blockForEventData(); + return processMessage(new JSONObject(message)); + } catch (final JSONException e) { + throw new IllegalStateException("Invalid message", e); + } + } + + /** + * Process a message if a new or saved message is available. + * + * @return MessageStatus value to indicate result of processing the message + */ + private MessageStatus maybeProcessPendingMessage() { + // We're on the test thread. + final String message = mExpecter.blockForEventDataWithTimeout(0); + if (message != null) { + try { + return processMessage(new JSONObject(message)); + } catch (final JSONException e) { + throw new IllegalStateException("Invalid message", e); + } + } + if (mSavedAsyncMessage != null) { + // processMessage clears mSavedAsyncMessage. + return processMessage(mSavedAsyncMessage); + } + return MessageStatus.QUEUE_EMPTY; + } + + /** + * Wait for all asynchronous messages from Javascript to be processed. + */ + private void finishPendingCalls() { + MessageStatus result; + do { + result = maybeProcessPendingMessage(); + if (result == MessageStatus.REPLIED) { + throw new IllegalStateException("Sync reply was unexpected"); + } + } while (result != MessageStatus.QUEUE_EMPTY); + } + + private void ensureJavaBridgeLoaded() { + while (!mJavaBridgeLoaded) { + processPendingMessage(); + } + } + + private void sendMessage(final String innerType, final String method, final Object[] args) { + ensureJavaBridgeLoaded(); + + // Call from Java to Javascript + final JSONObject message = new JSONObject(); + final JSONArray jsonArgs = new JSONArray(); + try { + if (args != null) { + for (final Object arg : args) { + jsonArgs.put(convertToJSONValue(arg)); + } + } + message.put("type", EVENT_TYPE) + .put("innerType", innerType) + .put("method", method) + .put("args", jsonArgs); + } catch (final JSONException e) { + throw new IllegalStateException("Unable to create JSON message", e); + } + sActions.sendGeckoEvent(EVENT_TYPE, message.toString()); + } + + private MessageStatus processMessage(JSONObject message) { + final String type; + final String methodName; + final JSONArray argsArray; + final Object[] args; + try { + if (!EVENT_TYPE.equals(message.getString("type"))) { + throw new IllegalStateException("Message type is not " + EVENT_TYPE); + } + type = message.getString("innerType"); + + switch (type) { + case "progress": + // Javascript harness message + mLogParser.logMessage(message.getString("message")); + return MessageStatus.PROCESSED; + + case "notify-loaded": + mJavaBridgeLoaded = true; + return MessageStatus.PROCESSED; + + case "sync-reply": + // Reply to Java-to-Javascript sync call + return MessageStatus.REPLIED; + + case "sync-call": + case "async-call": + + if ("async-call".equals(type)) { + // Save this async message until another async message arrives, then we + // process the saved message and save the new one. This is done as a + // form of tail call optimization, by making sync-replies come before + // async-calls. On the other hand, if (message == mSavedAsyncMessage), + // it means we're currently processing the saved message and should clear + // mSavedAsyncMessage. + final JSONObject newSavedMessage = + (message != mSavedAsyncMessage ? message : null); + message = mSavedAsyncMessage; + mSavedAsyncMessage = newSavedMessage; + if (message == null) { + // Saved current message and there wasn't an already saved one. + return MessageStatus.SAVED; + } + } + + methodName = message.getString("method"); + argsArray = message.getJSONArray("args"); + args = new Object[argsArray.length()]; + for (int i = 0; i < args.length; i++) { + args[i] = convertFromJSONValue(argsArray.get(i)); + } + invokeMethod(methodName, args); + + if ("sync-call".equals(type)) { + // Reply for sync messages + sendMessage("sync-reply", methodName, null); + } + return MessageStatus.PROCESSED; + } + + throw new IllegalStateException("Message type is unexpected"); + + } catch (final JSONException e) { + throw new IllegalStateException("Unable to retrieve JSON message", e); + } + } + + /** + * Given a method name and a list of arguments, + * call the most suitable method in the subclass. + */ + private Object invokeMethod(final String methodName, final Object[] args) { + final Class<?>[] argTypes = new Class<?>[args.length]; + for (int i = 0; i < argTypes.length; i++) { + if (args[i] == null) { + argTypes[i] = Object.class; + } else { + argTypes[i] = args[i].getClass(); + } + } + + // Try using argument types directly without casting. + try { + return invokeMethod(mTarget.getClass().getMethod(methodName, argTypes), args); + } catch (final NoSuchMethodException e) { + // getMethod() failed; try fallback below. + } + + // One scenario for getMethod() to fail above is that we don't have the exact + // argument types in argTypes (e.g. JS gave us an int but we're using a double, + // or JS gave us a null and we don't know its intended type), or the number of + // arguments is incorrect. Now we find all the methods with the given name and + // try calling them one-by-one. If one call fails, we move to the next call. + // Java will try to convert our arguments to the right types. + Throwable lastException = null; + for (final Method method : mMethods) { + if (!method.getName().equals(methodName)) { + continue; + } + try { + return invokeMethod(method, args); + } catch (final IllegalArgumentException e) { + lastException = e; + // Try the next method + } catch (final UnsupportedOperationException e) { + // "Cannot access method" exception below, see if there are other public methods + lastException = e; + // Try the next method + } + } + // Now we're out of options + throw new UnsupportedOperationException( + "Cannot call method " + methodName + " (not public? wrong argument types?)", + lastException); + } + + private Object invokeMethod(final Method method, final Object[] args) { + try { + return method.invoke(mTarget, args); + } catch (final IllegalAccessException e) { + throw new UnsupportedOperationException( + "Cannot access method " + method.getName(), e); + } catch (final InvocationTargetException e) { + final Throwable cause = e.getCause(); + if (cause instanceof CallException) { + // Don't wrap CallExceptions; this can happen if a call is nested on top + // of existing sync calls, and the nested call throws a CallException + throw (CallException) cause; + } + throw new CallException("Failed to invoke " + method.getName(), cause); + } + } + + private Object convertFromJSONValue(final Object value) { + if (value == JSONObject.NULL) { + return null; + } + return value; + } + + private Object convertToJSONValue(final Object value) { + if (value == null) { + return JSONObject.NULL; + } + return value; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java new file mode 100644 index 000000000..6237f1adc --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/JavascriptMessageParser.java @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import org.mozilla.gecko.Assert; + +import junit.framework.AssertionFailedError; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Route messages from Javascript's head.js test framework into Java's + * Mochitest framework. + */ +public final class JavascriptMessageParser { + + /** + * The Javascript test harness sends test events to Java. + * Each such test event is wrapped in a Robocop:JS event. + */ + public static final String EVENT_TYPE = "Robocop:JS"; + + // Messages matching this pattern are handled specially. Messages not + // matching this pattern are still printed. This pattern should be able + // to handle having multiple lines in a message. + private static final Pattern testMessagePattern = + Pattern.compile("TEST-([A-Z\\-]+) \\| (.*?) \\| (.*)", Pattern.DOTALL); + + private final Assert asserter; + // Used to help print stack traces neatly. + private String lastTestName = ""; + // Have we seen a message saying the test is finished? + private boolean testFinishedMessageSeen = false; + private final boolean endOnAssertionFailure; + + /** + * Constructs a message parser for test result messages sent from JavaScript. When seeing an + * assertion failure, the message parser can use the given {@link org.mozilla.gecko.Assert} + * instance to immediately end the test (typically if the underlying JS framework is not able + * to end the test itself) or to swallow the Errors - this functionality is determined by the + * <code>endOnAssertionFailure</code> parameter. + * + * @param asserter The Assert instance to which test results should be passed. + * @param endOnAssertionFailure + * true if the test should end if we see a JS assertion failure, false otherwise. + */ + public JavascriptMessageParser(final Assert asserter, final boolean endOnAssertionFailure) { + this.asserter = asserter; + this.endOnAssertionFailure = endOnAssertionFailure; + } + + public boolean isTestFinished() { + return testFinishedMessageSeen; + } + + public void logMessage(final String str) { + final Matcher m = testMessagePattern.matcher(str.trim()); + + if (m.matches()) { + final String type = m.group(1); + final String name = m.group(2); + final String message = m.group(3); + + if ("INFO".equals(type)) { + asserter.info(name, message); + testFinishedMessageSeen = testFinishedMessageSeen || + "exiting test".equals(message); + } else if ("PASS".equals(type)) { + asserter.ok(true, name, message); + } else if ("UNEXPECTED-FAIL".equals(type)) { + try { + asserter.ok(false, name, message); + } catch (AssertionFailedError e) { + // Above, we call the assert, allowing it to log. + // Now we can end the test, if applicable. + if (this.endOnAssertionFailure) { + throw e; + } + // Otherwise, swallow the Error. The JS framework we're + // logging messages from is likely capable of ending tests + // when it needs to, and we want to see all of its failures, + // not just the first one! + } + } else if ("KNOWN-FAIL".equals(type)) { + asserter.todo(false, name, message); + } else if ("UNEXPECTED-PASS".equals(type)) { + asserter.todo(true, name, message); + } + + lastTestName = name; + } else { + // Generally, these extra lines are stack traces from failures, + // so we print them with the name of the last test seen. + asserter.info(lastTestName, str.trim()); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java new file mode 100644 index 000000000..e3ccc8236 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/NavigationHelper.java @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; + +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.tests.UITestContext.ComponentType; +import org.mozilla.gecko.tests.components.AppMenuComponent; +import org.mozilla.gecko.tests.components.ToolbarComponent; + +import com.robotium.solo.Solo; + +/** + * Provides helper functionality for navigating around the Firefox UI. These functions will often + * combine actions taken on multiple components to perform larger interactions. + */ +final public class NavigationHelper { + private static UITestContext sContext; + private static Solo sSolo; + + private static AppMenuComponent sAppMenu; + private static ToolbarComponent sToolbar; + + protected static void init(final UITestContext context) { + sContext = context; + sSolo = context.getSolo(); + + sAppMenu = (AppMenuComponent) context.getComponent(ComponentType.APPMENU); + sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR); + } + + public static void enterAndLoadUrl(String url) { + fAssertNotNull("url is not null", url); + + url = adjustUrl(url); + sToolbar.enterEditingMode() + .enterUrl(url) + .commitEditingMode(); + } + + /** + * Returns a new URL with the docshell HTTP server host prefix. + */ + public static String adjustUrl(final String url) { + fAssertNotNull("url is not null", url); + + if (url.startsWith("about:") || url.startsWith("chrome:")) { + return url; + } + + return sContext.getAbsoluteHostnameUrl(url); + } + + public static void goBack() { + if (DeviceHelper.isTablet()) { + sToolbar.pressBackButton(); // Waits for page load & asserts isNotEditing. + return; + } + + sToolbar.assertIsNotEditing(); + WaitHelper.waitForPageLoad(new Runnable() { + @Override + public void run() { + // TODO: Lower soft keyboard first if applicable. Note that + // Solo.hideSoftKeyboard() does not clear focus (which might be fine since + // Gecko would be the element focused). + sSolo.goBack(); + } + }); + } + + public static void goForward() { + if (DeviceHelper.isTablet()) { + sToolbar.pressForwardButton(); // Waits for page load & asserts isNotEditing. + return; + } + + sToolbar.assertIsNotEditing(); + WaitHelper.waitForPageLoad(new Runnable() { + @Override + public void run() { + sAppMenu.pressMenuItem(AppMenuComponent.MenuItem.FORWARD); + } + }); + } + + public static void reload() { + if (DeviceHelper.isTablet()) { + sToolbar.pressReloadButton(); // Waits for page load & asserts isNotEditing. + return; + } + + sToolbar.assertIsNotEditing(); + WaitHelper.waitForPageLoad(new Runnable() { + @Override + public void run() { + sAppMenu.pressMenuItem(AppMenuComponent.MenuItem.RELOAD); + } + }); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java new file mode 100644 index 000000000..2536eb9db --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/RobotiumHelper.java @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import com.robotium.solo.Solo; + +import org.mozilla.gecko.tests.UITestContext; + +import java.util.regex.Pattern; + +/** + * Provides helper functions for using Robotium. + */ +public final class RobotiumHelper { + private static Solo sSolo; + + private RobotiumHelper() { /* To disallow instantiation. */ } + + protected static void init(final UITestContext context) { + sSolo = context.getSolo(); + } + + /** + * Same as Solo.waitForText(), but matching against full text, without regular expressions. + */ + public static boolean waitForExactText(final String text, + final int minimumNumberOfMatches, + final long timeout) { + String matchText = "^" + Pattern.quote(text) + "$"; + return sSolo.waitForText(matchText, minimumNumberOfMatches, timeout); + } + + /** + * Same as Solo.searchText(), but matching against full text, without regular expressions. + */ + public static boolean searchExactText(final String text, + final boolean onlyVisible) { + String matchText = "^" + Pattern.quote(text) + "$"; + return sSolo.searchText(matchText, onlyVisible); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java new file mode 100644 index 000000000..f6e616652 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/WaitHelper.java @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests.helpers; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +import android.os.SystemClock; + +import java.util.concurrent.Callable; +import java.util.regex.Pattern; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Actions.EventExpecter; +import org.mozilla.gecko.tests.UITestContext; +import org.mozilla.gecko.tests.UITestContext.ComponentType; +import org.mozilla.gecko.tests.components.ToolbarComponent; + +import com.robotium.solo.Condition; +import com.robotium.solo.Solo; + +/** + * Provides functionality related to waiting on certain events to happen. + */ +public final class WaitHelper { + // TODO: Make public for when Solo.waitForCondition is used directly (i.e. do not want + // assertion from waitFor)? + // DEFAULT_MAX_WAIT_MS of 5000 was intermittently insufficient during + // initialization on Android 2.3 emulator -- bug 1114655 + private static final int DEFAULT_MAX_WAIT_MS = 15000; + private static final int PAGE_LOAD_WAIT_MS = 10000; + private static final int CHANGE_WAIT_MS = 15000; + + // TODO: via lucasr - Add ThrobberVisibilityChangeVerifier? + private static final ChangeVerifier[] PAGE_LOAD_VERIFIERS = new ChangeVerifier[] { + new ToolbarTitleTextChangeVerifier() + }; + + private static UITestContext sContext; + private static Solo sSolo; + private static Actions sActions; + + private static ToolbarComponent sToolbar; + + private WaitHelper() { /* To disallow instantiation. */ } + + protected static void init(final UITestContext context) { + sContext = context; + sSolo = context.getSolo(); + sActions = context.getActions(); + + sToolbar = (ToolbarComponent) context.getComponent(ComponentType.TOOLBAR); + } + + /** + * Waits for the given {@link solo.Condition} using the default wait duration; will throw an + * AssertionError if the duration is elapsed and the condition is not satisfied. + */ + public static void waitFor(String message, final Condition condition) { + message = "Waiting for " + message + "."; + fAssertTrue(message, sSolo.waitForCondition(condition, DEFAULT_MAX_WAIT_MS)); + } + + /** + * Waits for the given {@link solo.Condition} using the given wait duration; will throw an + * AssertionError if the duration is elapsed and the condition is not satisfied. + */ + public static void waitFor(String message, final Condition condition, final int waitMillis) { + message = "Waiting for " + message + " with timeout " + waitMillis + "."; + fAssertTrue(message, sSolo.waitForCondition(condition, waitMillis)); + } + + /** + * Waits for the given Callable to return something that is not null, using the given wait + * duration; will throw an AssertionError if the duration is elapsed and the callable has not + * returned a non-null object. + * + * @return the value returned by the Callable. Or null if the duration has elapsed. + */ + public static <V> V waitFor(String message, final Callable<V> callable, int waitMillis) { + sContext.dumpLog("WaitHelper", "Waiting for " + message + " with timeout " + waitMillis + "."); + + final Object[] value = new Object[1]; + + Condition condition = new Condition() { + @Override + public boolean isSatisfied() { + try { + V result = callable.call(); + value[0] = result; + return result != null; + } catch (Exception e) { + return false; + } + } + }; + + sSolo.waitForCondition(condition, waitMillis); + + return (V) value[0]; + } + + /** + * Waits for the Gecko event declaring the page has loaded. Takes in and runs a Runnable + * that will perform the action that will cause the page to load. + */ + public static void waitForPageLoad(final Runnable initiatingAction) { + fAssertNotNull("initiatingAction is not null", initiatingAction); + + // Some changes to the UI occur in response to the same event we listen to for when + // the page has finished loading (e.g. a page title update). As such, we ensure this + // UI state has changed before returning from this method; here we store the initial + // state. + final ChangeVerifier[] pageLoadVerifiers = PAGE_LOAD_VERIFIERS; + for (final ChangeVerifier verifier : pageLoadVerifiers) { + verifier.storeState(); + } + + // Wait for the page load and title changed event. + final EventExpecter[] eventExpecters = new EventExpecter[] { + sActions.expectGeckoEvent("DOMContentLoaded"), + sActions.expectGeckoEvent("DOMTitleChanged") + }; + + initiatingAction.run(); + + // PAGE_LOAD_WAIT_MS is the total time we wait for all events to finish. + final long expecterStartMillis = SystemClock.uptimeMillis(); + for (final EventExpecter expecter : eventExpecters) { + final int eventWaitTimeMillis = PAGE_LOAD_WAIT_MS - (int)(SystemClock.uptimeMillis() - expecterStartMillis); + expecter.blockForEventDataWithTimeout(eventWaitTimeMillis); + expecter.unregisterListener(); + } + + // The timeout wait time should be the aggregate time for all ChangeVerifiers. + final long verifierStartMillis = SystemClock.uptimeMillis(); + + // Verify remaining state has changed. + for (final ChangeVerifier verifier : pageLoadVerifiers) { + // If we timeout, either the state is set to the same value (which is fine), or + // the state has not yet changed. Since we can't be sure it will ever change, move + // on and let the assertions fail if applicable. + final int verifierWaitMillis = CHANGE_WAIT_MS - (int)(SystemClock.uptimeMillis() - verifierStartMillis); + final boolean hasTimedOut = !sSolo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return verifier.hasStateChanged(); + } + }, verifierWaitMillis); + + sContext.dumpLog(verifier.getLogTag(), + (hasTimedOut ? "timed out." : "was satisfied.")); + } + } + + /** + * Implementations of this interface verify that the state of the test has changed from + * the invocation of storeState to the invocation of hasStateChanged. A boolean will be + * returned from hasStateChanged, indicating this change of status. + */ + private interface ChangeVerifier { + String getLogTag(); + + /** + * Stores the initial state of the system. This system state is used to diff against + * the end state to determine if the system has changed. Since this is just a diff + * (with a timeout), this method could potentially store state inconsistent with + * what is visible to the user. + */ + void storeState(); + boolean hasStateChanged(); + } + + private static class ToolbarTitleTextChangeVerifier implements ChangeVerifier { + private static final String LOGTAG = ToolbarTitleTextChangeVerifier.class.getSimpleName(); + + // A regex that matches the page title that shows up while the page is loading. + private static final Pattern LOADING_PREFIX = Pattern.compile("[A-Za-z]{3,9}://"); + + private CharSequence mOldTitleText; + + @Override + public String getLogTag() { + return LOGTAG; + } + + @Override + public void storeState() { + mOldTitleText = sToolbar.getPotentiallyInconsistentTitle(); + sContext.dumpLog(LOGTAG, "stored title, \"" + mOldTitleText + "\"."); + } + + @Override + public boolean hasStateChanged() { + // TODO: Additionally, consider Solo.waitForText. + // TODO: Robocop sleeps .5 sec between calls. Cache title view? + final CharSequence title = sToolbar.getPotentiallyInconsistentTitle(); + + // TODO: Handle the case where the URL is shown instead of page title by preference. + // HACK: We want to wait until the title changes to the state a tester may assert + // (e.g. the page title). However, the title is set to the URL before the title is + // loaded from the server and set as the final page title; we ignore the + // intermediate URL loading state here. + final boolean isLoading = LOADING_PREFIX.matcher(title).lookingAt(); + final boolean hasStateChanged = !isLoading && !mOldTitleText.equals(title); + + if (hasStateChanged) { + sContext.dumpLog(LOGTAG, "state changed to title, \"" + title + "\"."); + } + return hasStateChanged; + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java new file mode 100644 index 000000000..e3afeb8d9 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testANRReporter.java @@ -0,0 +1,240 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.AppConstants; + +import android.content.Context; +import android.content.Intent; + +import com.robotium.solo.Condition; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; + +import org.json.JSONObject; + +/** + * Tests the proper operation of the ANR reporter. + */ +public class testANRReporter extends BaseTest { + + private static final String ANR_ACTION = "android.intent.action.ANR"; + private static final String PING_DIR = "saved-telemetry-pings"; + private static final int WAIT_FOR_PING_TIMEOUT = 60000; + private static final String ANR_PATH = "/data/anr/traces.txt"; + private static final String SAMPLE_ANR + = "----- pid 1 at 2014-01-15 18:55:51 -----\n" + + "Cmd line: " + AppConstants.ANDROID_PACKAGE_NAME + "\n" + + "\n" + + "JNI: CheckJNI is off; workarounds are off; pins=0; globals=397\n" + + "\n" + + "DALVIK THREADS:\n" + + "(mutexes: tll=0 tsl=0 tscl=0 ghl=0)\n" + + "\n" + + "\"main\" prio=5 tid=1 WAIT\n" + + " | group=\"main\" sCount=1 dsCount=0 obj=0x41d6bc90 self=0x41d5a3c8\n" + + " | sysTid=3485 nice=0 sched=0/0 cgrp=apps handle=1074852180\n" + + " | state=S schedstat=( 0 0 0 ) utm=1065 stm=152 core=0\n" + + " at java.lang.Object.wait(Native Method)\n" + + " - waiting on <0x427ab340> (a org.mozilla.gecko.GeckoEditable$5)\n" + + " at java.lang.Object.wait(Object.java:364)\n" + + " at org.mozilla.gecko.GeckoEditable$5.run(GeckoEditable.java:746)\n" + + " at android.os.Handler.handleCallback(Handler.java:733)\n" + + " at android.os.Handler.dispatchMessage(Handler.java:95)\n" + + " at android.os.Looper.loop(Looper.java:137)\n" + + " at android.app.ActivityThread.main(ActivityThread.java:4998)\n" + + " at java.lang.reflect.Method.invokeNative(Native Method)\n" + + " at java.lang.reflect.Method.invoke(Method.java:515)\n" + + " at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)\n" + + " at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)\n" + + " at dalvik.system.NativeStart.main(Native Method)\n" + + "\n" + + "\"Gecko\" prio=5 tid=16 SUSPENDED\n" + + " | group=\"main\" sCount=1 dsCount=0 obj=0x426e2b28 self=0x76ae92e8\n" + + " | sysTid=3541 nice=0 sched=0/0 cgrp=apps handle=1991153472\n" + + " | state=S schedstat=( 0 0 0 ) utm=1118 stm=145 core=0\n" + + " #00 pc 00000904 /system/lib/libc.so (__futex_syscall3+4294832136)\n" + + " #01 pc 0000eec4 /system/lib/libc.so (__pthread_cond_timedwait_relative+48)\n" + + " #02 pc 0000ef24 /system/lib/libc.so (__pthread_cond_timedwait+64)\n" + + " #03 pc 000536b7 /system/lib/libdvm.so\n" + + " #04 pc 00053c79 /system/lib/libdvm.so (dvmChangeStatus(Thread*, ThreadStatus)+34)\n" + + " #05 pc 00049507 /system/lib/libdvm.so\n" + + " #06 pc 0004d84b /system/lib/libdvm.so\n" + + " #07 pc 0003f1df /dev/ashmem/libxul.so (deleted)\n" + + " at org.mozilla.gecko.mozglue.GeckoLoader.nativeRun(Native Method)\n" + + " at org.mozilla.gecko.GeckoAppShell.runGecko(GeckoAppShell.java:384)\n" + + " at org.mozilla.gecko.GeckoThread.run(GeckoThread.java:177)\n" + + "\n" + + "----- end 1 -----\n" + + "\n" + + "\n" + + "----- pid 2 at 2013-01-25 13:27:01 -----\n" + + "Cmd line: system_server\n" + + "\n" + + "----- end 2 -----\n"; + + private boolean mDone; + + private JSONObject readPingFile(final File pingFile) throws Exception { + final long fileSize = pingFile.length(); + if (fileSize == 0 || fileSize > Integer.MAX_VALUE) { + throw new Exception("Invalid ping file size"); + } + final char[] buffer = new char[(int) fileSize]; + final FileReader reader = new FileReader(pingFile); + try { + final int readSize = reader.read(buffer); + if (readSize == 0 || readSize > buffer.length) { + throw new Exception("Invalid number of bytes read"); + } + } finally { + reader.close(); + } + return new JSONObject(new String(buffer)); + } + + public void testANRReporter() throws Exception { + blockForGeckoReady(); + + // Cannot test ANR reporter if it's disabled. + if (!AppConstants.MOZ_ANDROID_ANR_REPORTER) { + mAsserter.ok(true, "ANR reporter is disabled", null); + return; + } + + // For the ANR reporter to work, we need to provide sample ANR traces to it. + // Therefore, we need the ANR file to exist and writable. If not, we don't + // have the right permissions to create the file, so we just bail. + final File anrFile = new File(ANR_PATH); + if (!anrFile.exists()) { + mAsserter.ok(true, "ANR file does not exist", null); + return; + } + if (!anrFile.canWrite()) { + mAsserter.ok(true, "ANR file is not writable", null); + return; + } + + final FileWriter anrWriter = new FileWriter(anrFile); + try { + anrWriter.write(SAMPLE_ANR); + } finally { + anrWriter.close(); + } + + // Block the UI thread to simulate an ANR + final Runnable uiBlocker = new Runnable() { + @Override + public synchronized void run() { + while (!mDone) { + try { + wait(); + } catch (final InterruptedException e) { + } + } + } + }; + getActivity().runOnUiThread(uiBlocker); + + // Make sure our initial ping directory is empty. + final File pingDir = new File(mProfile, PING_DIR); + final String[] initialFiles = pingDir.list(); + mAsserter.ok(initialFiles == null || initialFiles.length == 0, + "Ping directory is empty", null); + + final Intent anrIntent = new Intent(ANR_ACTION); + anrIntent.setPackage(AppConstants.ANDROID_PACKAGE_NAME); + mAsserter.is(anrIntent.getPackage(), AppConstants.ANDROID_PACKAGE_NAME, + "Successfully set package name"); + + final Context testContext = getInstrumentation().getContext(); + mAsserter.isnot(testContext, null, "testContext should not be null"); + + // Trigger the ANR. + mAsserter.info("Triggering ANR", null); + testContext.sendBroadcast(anrIntent); + + // ANR reporter is supposed to ignore duplicate ANRs. + // This will be checked later when we look for ping files. + mAsserter.info("Triggering second ANR", null); + testContext.sendBroadcast(new Intent(anrIntent)); + + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + mAsserter.info("Waiting for ping", null); + + try { + // Sleep to allow the ANR reporter thread time to process the ANR. + Thread.sleep(1000); + } catch (final InterruptedException e) { + } + + final File[] newFiles = pingDir.listFiles(); + if (newFiles == null || newFiles.length == 0) { + // Keep waiting. + return false; + } + // Make sure we have a complete file. We skip assertions and catch all + // exceptions here because the condition may not be satisfied now but may + // be satisfied later. After the wait is over, we will repeat the same + // steps with assertions and exceptions. + try { + return readPingFile(newFiles[0]).has("slug"); + } catch (final Exception e) { + return false; + } + } + }, WAIT_FOR_PING_TIMEOUT); + + mAsserter.ok(pingDir.exists(), "Ping directory exists", null); + mAsserter.ok(pingDir.isDirectory(), "Ping directory is a directory", null); + + final File[] newFiles = pingDir.listFiles(); + mAsserter.isnot(newFiles, null, "Ping directory is not empty"); + mAsserter.is(newFiles.length, 1, "ANR reporter wrote one ping"); + mAsserter.ok(newFiles[0].exists(), "Ping exists", null); + mAsserter.ok(newFiles[0].isFile(), "Ping is a file", null); + mAsserter.ok(newFiles[0].canRead(), "Ping is readable", null); + mAsserter.info("Found ping file", newFiles[0].getPath()); + + // Check standard properties required by Telemetry server. + final JSONObject pingObject = readPingFile(newFiles[0]); + mAsserter.ok(pingObject.has("slug"), "Ping has slug property", null); + mAsserter.ok(pingObject.has("reason"), "Ping has reason property", null); + mAsserter.ok(pingObject.has("payload"), "Ping has payload property", null); + + final JSONObject pingPayload = pingObject.getJSONObject("payload"); + mAsserter.ok(pingPayload.has("ver"), "Payload has ver property", null); + mAsserter.ok(pingPayload.has("info"), "Payload has info property", null); + mAsserter.ok(pingPayload.has("androidANR"), "Payload has androidANR property", null); + + final JSONObject pingInfo = pingPayload.getJSONObject("info"); + mAsserter.ok(pingInfo.has("reason"), "Info has reason property", null); + mAsserter.ok(pingInfo.has("appName"), "Info has appName property", null); + mAsserter.ok(pingInfo.has("appUpdateChannel"), "Info has appUpdateChannel property", null); + mAsserter.ok(pingInfo.has("appVersion"), "Info has appVersion property", null); + mAsserter.ok(pingInfo.has("appBuildID"), "Info has appBuildID property", null); + + // Do some profile clean up. This is not absolutely necessary because the profile + // is blown away after test runs anyways, so we don't check return values here. + for (final File ping : newFiles) { + ping.delete(); + } + pingDir.delete(); + + // Unblock UI thread + synchronized (uiBlocker) { + mDone = true; + uiBlocker.notify(); + } + + // Clear the sample ANR + final FileWriter anrClearer = new FileWriter(anrFile); + anrClearer.close(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java new file mode 100644 index 000000000..68f3a38db --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.home.HomeConfig.PanelType; +import org.mozilla.gecko.tests.helpers.DeviceHelper; +import org.mozilla.gecko.tests.helpers.GeckoHelper; + +/** + * Tests functionality related to navigating between the various about:home panels. + */ +public class testAboutHomePageNavigation extends UITest { + // TODO: Define this test dynamically by creating dynamic representations of the Page + // enum for both phone and tablet, then swiping through the panels. This will also + // benefit having a HomePager with custom panels. + public void testAboutHomePageNavigation() { + GeckoHelper.blockForDelayedStartup(); + + mAboutHome.assertVisible() + .assertCurrentPanel(PanelType.TOP_SITES); + + mAboutHome.swipeToPanelOnRight(); + mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS); + + // Ideally these helpers would just be their own tests. However, by keeping this within + // one method, we're saving test setUp and tearDown resources. + if (DeviceHelper.isTablet()) { + helperTestTablet(); + } else { + helperTestPhone(); + } + } + + private void helperTestTablet() { + mAboutHome.swipeToPanelOnRight(); + mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY); + + // Edge case. + mAboutHome.swipeToPanelOnRight(); + mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY); + + mAboutHome.swipeToPanelOnLeft(); + mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS); + + mAboutHome.swipeToPanelOnLeft(); + mAboutHome.assertCurrentPanel(PanelType.TOP_SITES); + + // Edge case. + mAboutHome.swipeToPanelOnLeft(); + mAboutHome.assertCurrentPanel(PanelType.TOP_SITES); + } + + private void helperTestPhone() { + // Edge case. + mAboutHome.swipeToPanelOnLeft(); + mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS); + + mAboutHome.swipeToPanelOnLeft(); + mAboutHome.assertCurrentPanel(PanelType.TOP_SITES); + + mAboutHome.swipeToPanelOnLeft(); + mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY); + + // Edge case. + mAboutHome.swipeToPanelOnLeft(); + mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY); + + mAboutHome.swipeToPanelOnRight(); + mAboutHome.assertCurrentPanel(PanelType.TOP_SITES); + } + + // TODO: bug 943706 - reimplement this old test code. + /* + // Removed by Bug 896576 - [fig] Remove [getAllPagesList] from BaseTest + // ListView list = getAllPagesList("about:firefox"); + + // Test normal sliding of the list left and right + ViewPager pager = (ViewPager)mSolo.getView(ViewPager.class, 0); + mAsserter.is(pager.getCurrentItem(), 0, "All pages is selected"); + + int width = mDriver.getGeckoWidth() / 2; + int y = mDriver.getGeckoHeight() / 2; + mActions.drag(width, 0, y, y); + mAsserter.is(pager.getCurrentItem(), 1, "Bookmarks page is selected"); + + mActions.drag(0, width, y, y); + mAsserter.is(pager.getCurrentItem(), 0, "All pages is selected"); + + // Test tapping on the tab strip changes tabs + TabWidget tabwidget = (TabWidget)mSolo.getView(TabWidget.class, 0); + mSolo.clickOnView(tabwidget.getChildAt(1)); + mAsserter.is(pager.getCurrentItem(), 1, "Clicking on tab selected bookmarks page"); + + // Test typing in the awesomebar changes tabs and prevents panning + mSolo.typeText(0, "woot"); + mAsserter.is(pager.getCurrentItem(), 0, "Searching switched to all pages tab"); + mSolo.scrollToSide(Solo.LEFT); + mAsserter.is(pager.getCurrentItem(), 0, "Dragging left is not allowed when searching"); + + mSolo.scrollToSide(Solo.RIGHT); + mAsserter.is(pager.getCurrentItem(), 0, "Dragging right is not allowed when searching"); + + mSolo.goBack(); + */ +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java new file mode 100644 index 000000000..3be6ed53f --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomeVisibility.java @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.home.HomeConfig; +import org.mozilla.gecko.home.HomeConfig.PanelType; +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +/** + * Tests the visibility of about:home after various interactions with the browser. + */ +public class testAboutHomeVisibility extends UITest { + public void testAboutHomeVisibility() { + GeckoHelper.blockForReady(); + + // Check initial state on about:home. + mToolbar.assertTitle(mStringHelper.ABOUT_HOME_URL); + mAboutHome.assertVisible() + .assertCurrentPanel(PanelType.TOP_SITES); + + // Go to blank 01. + NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + mAboutHome.assertNotVisible(); + + // Go to blank 02. + NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + mAboutHome.assertNotVisible(); + + // Enter editing mode, where the about:home UI should be visible. + mToolbar.enterEditingMode(); + mAboutHome.assertVisible() + .assertCurrentPanel(PanelType.TOP_SITES); + + // Dismiss editing mode, where the about:home UI should be gone. + mToolbar.dismissEditingMode(); + mAboutHome.assertNotVisible(); + + // Loading about:home should show about:home again. + NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL); + mToolbar.assertTitle(mStringHelper.ABOUT_HOME_URL); + mAboutHome.assertVisible() + .assertCurrentPanel(PanelType.TOP_SITES); + + // We can navigate to about:home panels by panel UUID. + mAboutHome.navigateToBuiltinPanelType(PanelType.BOOKMARKS) + .assertVisible() + .assertCurrentPanel(PanelType.BOOKMARKS); + mAboutHome.navigateToBuiltinPanelType(PanelType.COMBINED_HISTORY) + .assertVisible() + .assertCurrentPanel(PanelType.COMBINED_HISTORY); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java new file mode 100644 index 000000000..6a00acd96 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; + +/* Tests related to the about: page: + * - check that about: loads from the URL bar + * - check that about: loads from Settings/About... + */ +public class testAboutPage extends PixelTest { + + public void testAboutPage() { + blockForGeckoReady(); + + // Load the about: page and verify its title. + String url = mStringHelper.ABOUT_SCHEME; + loadAndPaint(url); + + verifyUrlInContentDescription(url); + + // Open a new page to remove the about: page from the current tab. + url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + loadUrlAndWait(url); + + // At this point the page title should have been set. + verifyUrlInContentDescription(url); + + // Set up listeners to catch the page load we're about to do. + Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added"); + Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded"); + + selectSettingsItem(mStringHelper.MOZILLA_SECTION_LABEL, mStringHelper.ABOUT_LABEL); + + // Wait for the new tab and page to load + tabEventExpecter.blockForEvent(); + contentEventExpecter.blockForEvent(); + + tabEventExpecter.unregisterListener(); + contentEventExpecter.unregisterListener(); + + // Make sure the about: page was loaded. + verifyUrlInContentDescription(mStringHelper.ABOUT_SCHEME); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java new file mode 100644 index 000000000..d064eb1dd --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java @@ -0,0 +1,76 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; + +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + + +public class testAccessibleCarets extends JavascriptTest { + private static final String LOGTAG = "testAccessibleCarets"; + private static final String TAB_CHANGE_EVENT = "testAccessibleCarets:TabChange"; + + private final TabsListener tabsListener; + + + public testAccessibleCarets() { + super("testAccessibleCarets.js"); + + tabsListener = new TabsListener(); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tabs.registerOnTabsChangedListener(tabsListener); + } + + @Override + public void tearDown() throws Exception { + Tabs.unregisterOnTabsChangedListener(tabsListener); + + super.tearDown(); + } + + @Override + public void testJavascript() throws Exception { + // This feature is currently only available in Nightly. + if (!AppConstants.NIGHTLY_BUILD) { + mAsserter.dumpLog(LOGTAG + " is disabled on non-Nightly builds: returning"); + return; + } + super.testJavascript(); + } + + /** + * Observes tab change events to broadcast to the test script. + */ + private class TabsListener implements Tabs.OnTabsChangedListener { + @Override + public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) { + switch (msg) { + case STOP: + final JSONObject args = new JSONObject(); + try { + args.put("tabId", tab.getId()); + args.put("event", msg.toString()); + } catch (JSONException e) { + Log.e(LOGTAG, "Error building JSON arguments for " + TAB_CHANGE_EVENT, e); + return; + } + mActions.sendGeckoEvent(TAB_CHANGE_EVENT, args.toString()); + break; + } + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java new file mode 100644 index 000000000..b4b06a236 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.support.design.widget.NavigationView; +import android.support.v4.app.Fragment; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.View; + +import com.robotium.solo.Condition; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.home.activitystream.ActivityStream; +import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu; + +/** + * This test is unfortunately closely coupled to the current implementation, however it is still + * useful in that it tests the bookmark/history state specific menu items for correctness. + */ +public class testActivityStreamContextMenu extends BaseTest { + public void testActivityStreamContextMenu() { + blockForGeckoReady(); + + final String testURL = "http://mozilla.org"; + + BrowserDB db = BrowserDB.from(getActivity()); + db.removeHistoryEntry(getActivity().getContentResolver(), testURL); + db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL); + + testMenuForUrl(testURL, false, false); + + db.addBookmark(getActivity().getContentResolver(), "foobar", testURL); + testMenuForUrl(testURL, true, false); + + db.updateVisitedHistory(getActivity().getContentResolver(), testURL); + testMenuForUrl(testURL, true, true); + + db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL); + testMenuForUrl(testURL, false, true); + } + + /** + * Test that the menu shows the expected menu items for a given URL, and that these items have + * the correct state. + */ + private void testMenuForUrl(final String url, final boolean isBookmarked, final boolean isVisited) { + final View anchor = new View(getActivity()); + + final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), anchor, ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, null, null, 100, 100); + + final int expectedBookmarkString; + if (isBookmarked) { + expectedBookmarkString = R.string.bookmark_remove; + } else { + expectedBookmarkString = R.string.bookmark; + } + + final MenuItem bookmarkItem = menu.getItemByID(R.id.bookmark); + assertMenuItemHasString(bookmarkItem, expectedBookmarkString); + + final MenuItem deleteItem = menu.getItemByID(R.id.delete); + assertMenuItemIsVisible(deleteItem, isVisited); + + menu.dismiss(); + } + + private void assertMenuItemIsVisible(final MenuItem item, final boolean shouldBeVisible) { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return (item.isVisible() == shouldBeVisible); + } + }, 5000); + + mAsserter.is(item.isVisible(), shouldBeVisible, "menu item \"" + item.getTitle() + "\" should be visible"); + } + + private void assertMenuItemHasString(final MenuItem item, final int stringID) { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return item.isEnabled(); + } + }, 5000); + + final String expectedTitle = getActivity().getResources().getString(stringID); + mAsserter.is(item.getTitle(), expectedTitle, "Title does not match expected title"); + } +} + diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java new file mode 100644 index 000000000..44bd1f903 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddSearchEngine.java @@ -0,0 +1,172 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; +import java.util.ArrayList; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.home.HomePager; +import org.mozilla.gecko.home.SearchEngineBar; +import org.mozilla.gecko.R; + +import android.widget.ImageView; +import android.widget.ListView; + +import com.robotium.solo.Condition; + +/** + * Test adding a search engine from an input field context menu. + * 1. Get the number of existing search engines from the SearchEngine:Data event and as displayed in about:home. + * 2. Load a page with a text field, open the context menu and add a search engine from the page. + * 3. Get the number of search engines after adding the new one and verify it has increased by 1. + */ +public class testAddSearchEngine extends AboutHomeTest { + private final int MAX_WAIT_TEST_MS = 5000; + private final String SEARCH_TEXT = "Firefox for Android"; + private final String ADD_SEARCHENGINE_OPTION_TEXT = "Add as Search Engine"; + + public void testAddSearchEngine() { + String blankPageURL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + String searchEngineURL = getAbsoluteUrl(mStringHelper.ROBOCOP_SEARCH_URL); + + blockForGeckoReady(); + int height = mDriver.getGeckoTop() + 150; + int width = mDriver.getGeckoLeft() + 150; + + inputAndLoadUrl(blankPageURL); + waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE); + + // Get the searchengine data by clicking the awesomebar - this causes Gecko to send Java the list + // of search engines. + Actions.EventExpecter searchEngineDataEventExpector = mActions.expectGeckoEvent("SearchEngines:Data"); + focusUrlBar(); + mActions.sendKeys(SEARCH_TEXT); + String eventData = searchEngineDataEventExpector.blockForEventData(); + searchEngineDataEventExpector.unregisterListener(); + + ArrayList<String> searchEngines; + try { + // Parse the data to get the number of searchengines. + searchEngines = getSearchEnginesNames(eventData); + } catch (JSONException e) { + mAsserter.ok(false, "Fatal exception in testAddSearchEngine while decoding JSON search engine string from Gecko prior to addition of new engine.", e.toString()); + return; + } + final int initialNumSearchEngines = searchEngines.size(); + mAsserter.dumpLog("Search Engines list = " + searchEngines.toString()); + + // Verify that the number of displayed search engines is the same as the one received through the SearchEngines:Data event. + verifyDisplayedSearchEnginesCount(initialNumSearchEngines); + + // Load the page for the search engine to add. + inputAndLoadUrl(searchEngineURL); + verifyUrlBarTitle(searchEngineURL); + + // Used to long-tap on the search input box for the search engine to add. + getInstrumentation().waitForIdleSync(); + mAsserter.dumpLog("Long Clicking at width = " + String.valueOf(width) + " and height = " + String.valueOf(height)); + mSolo.clickLongOnScreen(width,height); + + ImageView view = waitForViewWithDescription(ImageView.class, ADD_SEARCHENGINE_OPTION_TEXT); + mAsserter.isnot(view, null, "The action mode was opened"); + + // Add the search engine + mSolo.clickOnView(view); + waitForText("Cancel"); + clickOnButton("OK"); + mAsserter.ok(!mSolo.searchText(ADD_SEARCHENGINE_OPTION_TEXT), "Adding the Search Engine", "The add Search Engine pop-up has been closed"); + waitForText(mStringHelper.ROBOCOP_SEARCH_TITLE); // Make sure the pop-up is closed and we are back at the searchengine page + + // Load Robocop Blank 1 again to give the time for the searchengine to be added + // TODO: This is a potential source of intermittent oranges - it's a race condition! + loadUrl(blankPageURL); + waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE); + + // Load search engines again and check that the quantity of engines has increased by 1. + searchEngineDataEventExpector = mActions.expectGeckoEvent("SearchEngines:Data"); + focusUrlBar(); + mActions.sendKeys(SEARCH_TEXT); + eventData = searchEngineDataEventExpector.blockForEventData(); + + try { + // Parse the data to get the number of searchengines + searchEngines = getSearchEnginesNames(eventData); + } catch (JSONException e) { + mAsserter.ok(false, "Fatal exception in testAddSearchEngine while decoding JSON search engine string from Gecko after adding of new engine.", e.toString()); + return; + } + + mAsserter.dumpLog("Search Engines list = " + searchEngines.toString()); + mAsserter.is(searchEngines.size(), initialNumSearchEngines + 1, "Checking the number of Search Engines has increased"); + + // Verify that the number of displayed searchengines is the same as the one received through the SearchEngines:Data event. + verifyDisplayedSearchEnginesCount(initialNumSearchEngines + 1); + searchEngineDataEventExpector.unregisterListener(); + + // Verify that the search plugin XML file for the new engine ended up where we expected it to. + // This file name is created in nsSearchService.js based on the name of the new engine. + final File f = GeckoProfile.get(getActivity()).getFile("searchplugins/robocop-search-engine.xml"); + mAsserter.ok(f.exists(), "Checking that new search plugin file exists", ""); + } + + /** + * Helper method to decode a list of search engine names from the provided search engine information + * JSON string sent from Gecko. + * @param searchEngineData The JSON string representing the search engine array to process + * @return An ArrayList<String> containing the names of all the search engines represented in + * the provided JSON message. + * @throws JSONException In the event that the JSON provided cannot be decoded. + */ + public ArrayList<String> getSearchEnginesNames(String searchEngineData) throws JSONException { + JSONObject data = new JSONObject(searchEngineData); + JSONArray engines = data.getJSONArray("searchEngines"); + + ArrayList<String> searchEngineNames = new ArrayList<String>(); + for (int i = 0; i < engines.length(); i++) { + JSONObject engineJSON = engines.getJSONObject(i); + searchEngineNames.add(engineJSON.getString("name")); + } + return searchEngineNames; + } + + /** + * Method to verify that the displayed number of search engines matches the expected number. + * @param expectedCount The expected number of search engines. + */ + public void verifyDisplayedSearchEnginesCount(final int expectedCount) { + mSolo.clearEditText(0); + mActions.sendKeys(SEARCH_TEXT); + boolean correctNumSearchEnginesDisplayed = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + ListView searchResultList = findListViewWithTag(HomePager.LIST_TAG_BROWSER_SEARCH); + if (searchResultList == null || searchResultList.getAdapter() == null) { + return false; + } + + SearchEngineBar searchEngineBar = (SearchEngineBar) mSolo.getView(R.id.search_engine_bar); + if (searchEngineBar == null || searchEngineBar.getAdapter() == null) { + return false; + } + + final int actualCount = searchResultList.getAdapter().getCount() + + searchEngineBar.getAdapter().getItemCount() + - 1; // Subtract one for the search engine bar label (Bug 1172071) + + return (actualCount == expectedCount); + } + }, MAX_WAIT_TEST_MS); + + // Exit about:home + mSolo.goBack(); + waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE); + mAsserter.ok(correctNumSearchEnginesDisplayed, expectedCount + " Search Engines should be displayed" , "The correct number of Search Engines has been displayed"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java new file mode 100644 index 000000000..4256d93c4 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.json.JSONObject; +import org.mozilla.gecko.Actions; + +import android.util.DisplayMetrics; + +/** + * This test performs the following steps to check the behavior of the Add-on Manager: + * + * 1) Open the Add-on Manager from the Add-ons menu item, and then close it. + * 2) Open the Add-on Manager by visiting about:addons in the URL bar. + * 3) Open a new tab, select the Add-ons menu item, then verify that the existing + * Add-on Manager tab was selected, instead of opening a new tab. + */ +public class testAddonManager extends PixelTest { + public void testAddonManager() { + Actions.EventExpecter tabEventExpecter; + Actions.EventExpecter contentEventExpecter; + final String aboutAddonsURL = mStringHelper.ABOUT_ADDONS_URL; + + blockForGeckoReady(); + + // Use the menu to open the Addon Manger + selectMenuItem(mStringHelper.ADDONS_LABEL); + + // Set up listeners to catch the page load we're about to do + tabEventExpecter = mActions.expectGeckoEvent("Tab:Added"); + contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded"); + + // Wait for the new tab and page to load + tabEventExpecter.blockForEvent(); + contentEventExpecter.blockForEvent(); + + tabEventExpecter.unregisterListener(); + contentEventExpecter.unregisterListener(); + + // Verify the url + verifyUrlBarTitle(aboutAddonsURL); + + // Close the Add-on Manager + mSolo.goBack(); + + // Load the about:addons page and verify it was loaded + loadAndPaint(aboutAddonsURL); + verifyUrlBarTitle(aboutAddonsURL); + + // Setup wait for tab to spawn and load + tabEventExpecter = mActions.expectGeckoEvent("Tab:Added"); + contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded"); + + // Open a new tab + final String blankURL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + addTab(blankURL); + + // Wait for the new tab and page to load + tabEventExpecter.blockForEvent(); + contentEventExpecter.blockForEvent(); + + tabEventExpecter.unregisterListener(); + contentEventExpecter.unregisterListener(); + + // Verify tab count has increased + verifyTabCount(2); + + // Verify the page was opened + verifyUrlBarTitle(blankURL); + + // Addons Manager is not opened 2 separate times when opened from the menu + selectMenuItem(mStringHelper.ADDONS_LABEL); + + // Verify tab count not increased + verifyTabCount(2); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java new file mode 100644 index 000000000..13f7f817a --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAdobeFlash.java @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.json.JSONObject; +import org.mozilla.gecko.PaintedSurface; + +import android.os.Build; + +/** + * Tests that Flash is working + * - loads a page containing a Flash plugin + * - verifies it rendered properly + */ +public class testAdobeFlash extends PixelTest { + public void testLoad() { + // This test only works on ICS and higher + if (Build.VERSION.SDK_INT < 15) { + blockForGeckoReady(); + return; + } + + // Enable plugins + setPreferenceAndWaitForChange("plugin.enable", "1"); + + blockForGeckoReady(); + + String url = getAbsoluteUrl(mStringHelper.ROBOCOP_ADOBE_FLASH_URL); + PaintedSurface painted = loadAndGetPainted(url); + + mAsserter.ispixel(painted.getPixelAt(0, 0), 0, 0xff, 0, "Pixel at 0, 0"); + mAsserter.ispixel(painted.getPixelAt(50, 50), 0, 0xff, 0, "Pixel at 50, 50"); + mAsserter.ispixel(painted.getPixelAt(101, 0), 0xff, 0xff, 0xff, "Pixel at 101, 0"); + mAsserter.ispixel(painted.getPixelAt(0, 101), 0xff, 0xff, 0xff, "Pixel at 0, 101"); + + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java new file mode 100644 index 000000000..69efb4dec --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAppMenuPathways.java @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.json.JSONObject; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.tests.components.AppMenuComponent; +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +import com.robotium.solo.Solo; + +/** + * Set of tests to test UI App menu and submenus the user interact with. + */ +public class testAppMenuPathways extends UITest { + + /** + * Robocop supports only a single test function per test class. Therefore, we + * have a single top-level test function that dispatches to sub-tests. + */ + public void testAppMenuPathways() { + GeckoHelper.blockForReady(); + + _testHardwareMenuKeyOpenClose(); + _testSaveAsPDFPathway(); + } + + public void _testHardwareMenuKeyOpenClose() { + mAppMenu.assertMenuIsNotOpen(); + + mSolo.sendKey(Solo.MENU); + mAppMenu.waitForMenuOpen(); + mAppMenu.assertMenuIsOpen(); + + mSolo.sendKey(Solo.MENU); + mAppMenu.waitForMenuClose(); + mAppMenu.assertMenuIsNotOpen(); + } + + public void _testSaveAsPDFPathway() { + // Page menu should be disabled in about:home. + mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF); + + // Generate a mock Content:LocationChange message with video mime-type for the current tab (tabId = 0). + final JSONObject message = new JSONObject(); + try { + message.put("contentType", "video/webm"); + message.put("baseDomain", "webmfiles.org"); + message.put("type", "Content:LocationChange"); + message.put("sameDocument", false); + message.put("userRequested", ""); + message.put("uri", getAbsoluteIpUrl("/big-buck-bunny_trailer.webm")); + message.put("tabID", 0); + } catch (Exception ex) { + mAsserter.ok(false, "exception in testSaveAsPDFPathway", ex.toString()); + } + + // Mock video playback with the generated message and Content:LocationChange event. + Tabs.getInstance().handleMessage("Content:LocationChange", message); + + // Save as pdf menu is disabled while playing video. + mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF); + + // The above mock video playback test changes Java state, but not the associated JS state. + // Navigate to a new page so that the Java state is cleared. + NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + + // Test save as pdf functionality. + // The following call doesn't wait for the resulting pdf but checks that no exception are thrown. + // NOTE: save as pdf functionality must be done at the end as it is slow and cause other test operations to fail. + mAppMenu.pressMenuItem(AppMenuComponent.PageMenuItem.SAVE_AS_PDF); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java new file mode 100644 index 000000000..72bf62e04 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAxisLocking.java @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.PaintedSurface; + +/** + * Basic test for axis locking behaviour. + * - Load page and verify it draws + * - Drag page upwards 100 pixels at a 5-degree angle off the vertical axis + * - Verify that the 5-degree angle was thrown out and it dragged vertically + * - Drag page upwards at a 45-degree angle + * - Verify that the 45-degree angle was not thrown out and it dragged diagonally + */ +public class testAxisLocking extends PixelTest { + public void testAxisLocking() { + String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL); + + MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop()); + + blockForGeckoReady(); + + // load page and check we're at 0,0 + loadAndVerifyBoxes(url); + + // drag page upwards by 100 pixels with a slight angle. verify that + // axis locking prevents any horizontal scrolling + Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint(); + meh.dragSync(20, 150, 10, 50); + PaintedSurface painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + checkScrollWithBoxes(painted, 0, 100); + // since checkScrollWithBoxes only checks 4 points, it may not pick up a + // sub-100 pixel horizontal shift. so we check another point manually to make sure. + int[] color = getBoxColorAt(0, 100); + mAsserter.ispixel(painted.getPixelAt(99, 0), color[0], color[1], color[2], "Pixel at 99, 0 indicates no horizontal scroll"); + + // now drag at a 45-degree angle to ensure we break the axis lock, and + // verify that we have both horizontal and vertical scrolling + paintExpecter = mActions.expectPaint(); + meh.dragSync(150, 150, 50, 50); + } finally { + painted.close(); + } + + painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + checkScrollWithBoxes(painted, 100, 200); + } finally { + painted.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java new file mode 100644 index 000000000..b391f7920 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBackButtonInEditMode.java @@ -0,0 +1,47 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.tests.helpers.GeckoHelper; + +import android.view.View; + +/** + * Tests that verify the behavior of back button in edit mode. + */ +public class testBackButtonInEditMode extends UITest { + public void testBackButtonInEditMode() { + GeckoHelper.blockForReady(); + + // Verify back button behavior for edit mode. + mToolbar.enterEditingMode() + .assertIsUrlEditTextSelected(); + checkBackPressInEditMode(); + checkExitUsingBackButton(); + + // Verify back button behavior in edit mode after input. + mToolbar.enterEditingMode() + .enterUrl("dummy") + .assertIsUrlEditTextSelected(); + checkBackPressInEditMode(); + checkExitUsingBackButton(); + + // Verify the swipe behavior in edit mode. + mToolbar.enterEditingMode() + .assertIsUrlEditTextSelected(); + mAboutHome.swipeToPanelOnLeft(); + mToolbar.assertIsUrlEditTextNotSelected() + .assertIsEditing(); + checkExitUsingBackButton(); + } + + private void checkBackPressInEditMode() { + // Press back button and verify URLEditText is not selected. + getSolo().goBack(); + mToolbar.assertIsUrlEditTextNotSelected() + .assertIsEditing(); + } + + private void checkExitUsingBackButton() { + getSolo().goBack(); + mToolbar.assertIsNotEditing(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java new file mode 100644 index 000000000..041b76e2f --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmark.java @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import com.robotium.solo.Condition; + +public class testBookmark extends AboutHomeTest { + private static String BOOKMARK_URL; + private static final int WAIT_FOR_BOOKMARKED_TIMEOUT = 10000; + + public void testBookmark() { + BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + runAboutHomeTest(); + runMenuTest(); + } + + public void runMenuTest() { + mAsserter.is(mDatabaseHelper.isBookmark(BOOKMARK_URL), false, "Page is not bookmarked initially"); + setUpBookmark(); // loads the page, taps the star button, and waits for the "Bookmark Added" message + waitForBookmarked(true); + + cleanUpBookmark(); // loads the page, taps the star button, and waits for the "Bookmark Removed" message + waitForBookmarked(false); + } + + public void runAboutHomeTest() { + blockForGeckoReady(); + for (String url : mStringHelper.DEFAULT_BOOKMARKS_URLS) { + mAsserter.ok(mDatabaseHelper.isBookmark(url), "Checking that " + url + " is bookmarked by default", url + " is bookmarked"); + } + + mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL); + waitForBookmarked(true); + + isBookmarkDisplayed(BOOKMARK_URL); + loadBookmark(BOOKMARK_URL); + verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + + mDatabaseHelper.deleteBookmark(BOOKMARK_URL); + waitForBookmarked(false); + } + + private void waitForBookmarked(final boolean isBookmarked) { + boolean bookmarked = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return (isBookmarked) ? + mDatabaseHelper.isBookmark(BOOKMARK_URL) : + !mDatabaseHelper.isBookmark(BOOKMARK_URL); + } + }, WAIT_FOR_BOOKMARKED_TIMEOUT); + mAsserter.is(bookmarked, true, BOOKMARK_URL + " was " + (isBookmarked ? "added as a bookmark" : "removed from bookmarks")); + } + + private void setUpBookmark() { + // Bookmark a page for the test + loadUrl(BOOKMARK_URL); + waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE); + toggleBookmark(); + mAsserter.is(waitForText(mStringHelper.BOOKMARK_ADDED_LABEL), true, "bookmark added successfully"); + } + + private void cleanUpBookmark() { + // Go back to the page we bookmarked + loadUrl(BOOKMARK_URL); + waitForText(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE); + toggleBookmark(); + mAsserter.is(waitForText(mStringHelper.BOOKMARK_REMOVED_LABEL), true, "bookmark removed successfully"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java new file mode 100644 index 000000000..6205337ea --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkFolders.java @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.home.HomePager; +import org.mozilla.gecko.sync.Utils; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.net.Uri; +import android.view.View; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.robotium.solo.Condition; + +public class testBookmarkFolders extends AboutHomeTest { + private static String DESKTOP_BOOKMARK_URL; + + public void testBookmarkFolders() { + DESKTOP_BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + + setUpDesktopBookmarks(); + checkBookmarkList(); + } + + private void checkBookmarkList() { + openAboutHomeTab(AboutHomeTabs.BOOKMARKS); + waitForText(mStringHelper.DESKTOP_FOLDER_LABEL); + clickOnBookmarkFolder(mStringHelper.DESKTOP_FOLDER_LABEL); + waitForText(mStringHelper.TOOLBAR_FOLDER_LABEL); + + // Verify the number of folders displayed in the Desktop Bookmarks folder is correct + ListView desktopFolderContent = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS); + ListAdapter adapter = desktopFolderContent.getAdapter(); + + // Three folders and "Up to Bookmarks". + mAsserter.is(adapter.getCount(), 4, "Checking that the correct number of folders is displayed in the Desktop Bookmarks folder"); + + clickOnBookmarkFolder(mStringHelper.TOOLBAR_FOLDER_LABEL); + + // Go up in the bookmark folder hierarchy + clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.DESKTOP_FOLDER_LABEL)); + mAsserter.ok(waitForText(mStringHelper.BOOKMARKS_MENU_FOLDER_LABEL), "Going up in the folder hierarchy", "We are back in the Desktop Bookmarks folder"); + + clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.BOOKMARKS_ROOT_LABEL)); + mAsserter.ok(waitForText(mStringHelper.DESKTOP_FOLDER_LABEL), "Going up in the folder hierarchy", "We are back in the main Bookmarks List View"); + + clickOnBookmarkFolder(mStringHelper.DESKTOP_FOLDER_LABEL); + clickOnBookmarkFolder(mStringHelper.TOOLBAR_FOLDER_LABEL); + isBookmarkDisplayed(DESKTOP_BOOKMARK_URL); + + // Open the bookmark from a bookmark folder hierarchy + loadBookmark(DESKTOP_BOOKMARK_URL); + verifyUrlBarTitle(DESKTOP_BOOKMARK_URL); + openAboutHomeTab(AboutHomeTabs.BOOKMARKS); + + // Check that folders don't have a context menu + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + View desktopFolder = getBookmarkFolderView(mStringHelper.DESKTOP_FOLDER_LABEL); + if (desktopFolder == null) { + return false; + } + mSolo.clickLongOnView(desktopFolder); + return true; } + }, MAX_WAIT_MS); + + mAsserter.ok(success, "Trying to long click on the Desktop Bookmarks","Desktop Bookmarks folder could not be long clicked"); + + final String contextMenuString = mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[0]; + mAsserter.ok(!waitForText(contextMenuString), "Folders do not have context menus", "The context menu was not opened"); + + // Even if no context menu is opened long clicking a folder still opens it. We need to close it. + clickOnBookmarkFolder(String.format(mStringHelper.BOOKMARKS_UP_TO, mStringHelper.BOOKMARKS_ROOT_LABEL)); + } + + private void clickOnBookmarkFolder(final String folderName) { + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + View bookmarksFolder = getBookmarkFolderView(folderName); + if (bookmarksFolder == null) { + return false; + } + mSolo.waitForView(bookmarksFolder); + mSolo.clickOnView(bookmarksFolder); + return true; + } + }, MAX_WAIT_MS); + mAsserter.ok(success, "Trying to click on the " + folderName + " folder","The " + folderName + " folder was clicked"); + } + + private View getBookmarkFolderView(String folderName) { + openAboutHomeTab(AboutHomeTabs.BOOKMARKS); + mSolo.hideSoftKeyboard(); + getInstrumentation().waitForIdleSync(); + + ListView bookmarksTabList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS); + if (!waitForNonEmptyListToLoad(bookmarksTabList)) { + return null; + } + + ListAdapter adapter = bookmarksTabList.getAdapter(); + if (adapter == null) { + return null; + } + + for (int i = 0; i < adapter.getCount(); i++ ) { + View bookmarkView = bookmarksTabList.getChildAt(i); + if (bookmarkView instanceof TextView) { + TextView folderTextView = (TextView) bookmarkView; + if (folderTextView.getText().equals(folderName)) { + return bookmarkView; + } + } + } + + return null; + } + + // Add a bookmark in the Desktop folder so we can check the folder navigation in the bookmarks page + private void setUpDesktopBookmarks() { + blockForGeckoReady(); + + // Get the folder id of the mStringHelper.DESKTOP_FOLDER_LABEL folder + Long desktopFolderId = mDatabaseHelper.getFolderIdFromGuid("toolbar"); + + // Generate a Guid for the bookmark + final String generatedGuid = Utils.generateGuid(); + mAsserter.ok((generatedGuid != null), "Generating a random Guid for the bookmark", "We could not generate a Guid for the bookmark"); + + // Insert the bookmark + ContentResolver resolver = getActivity().getContentResolver(); + Uri bookmarksUri = mDatabaseHelper.buildUri(DatabaseHelper.BrowserDataType.BOOKMARKS); + + long now = System.currentTimeMillis(); + ContentValues values = new ContentValues(); + values.put("title", mStringHelper.ROBOCOP_BLANK_PAGE_02_TITLE); + values.put("url", DESKTOP_BOOKMARK_URL); + values.put("parent", desktopFolderId); + values.put("modified", now); + values.put("type", 1); + values.put("guid", generatedGuid); + values.put("position", 10); + values.put("created", now); + + int updated = resolver.update(bookmarksUri, + values, + "url = ?", + new String[] { DESKTOP_BOOKMARK_URL }); + if (updated == 0) { + Uri uri = resolver.insert(bookmarksUri, values); + mAsserter.ok(true, "Inserted at: ", uri.toString()); + } else { + mAsserter.ok(false, "Failed to insert the Desktop bookmark", "Something went wrong"); + } + } + + @Override + public void tearDown() throws Exception { + mDatabaseHelper.deleteBookmark(DESKTOP_BOOKMARK_URL); + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java new file mode 100644 index 000000000..363954bfa --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarkKeyword.java @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + + +public class testBookmarkKeyword extends AboutHomeTest { + public void testBookmarkKeyword() { + blockForGeckoReady(); + + final String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + final String keyword = "testkeyword"; + + // Add a bookmark, and update it to have a keyword. + mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, url); + mDatabaseHelper.updateBookmark(url, mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, keyword); + + // Enter the keyword in the urlbar. + inputAndLoadUrl(keyword); + + // Make sure the title of the page appeared. + verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + + // Delete the bookmark to clean up. + mDatabaseHelper.deleteBookmark(url); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java new file mode 100644 index 000000000..4ae57104c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarklets.java @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; + +import com.robotium.solo.Condition; + + +public class testBookmarklets extends BaseTest { + public void testBookmarklets() { + final String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + final String title = "alertBookmarklet"; + final String js = "javascript:alert(12 + 10)"; + final String expected = "22"; + boolean alerted; + + blockForGeckoReady(); + + // Load a standard page so bookmarklets work + loadUrlAndWait(url); + + // Verify that user-entered bookmarklets do *not* work + enterUrl(js); + mActions.sendSpecialKey(Actions.SpecialKey.ENTER); + alerted = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return mSolo.searchButton("OK", true) || mSolo.searchText(expected, true); + } + }, 3000); + mAsserter.is(alerted, false, "Alert was not shown for user-entered bookmarklet"); + + // Verify that non-user-entered bookmarklets do work + loadUrl(js); + alerted = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return mSolo.searchButton("OK", true) && mSolo.searchText(expected, true); + } + }, 10000); + mAsserter.is(alerted, true, "Alert was shown for bookmarklet"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java new file mode 100644 index 000000000..a7e9505da --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Element; +import org.mozilla.gecko.R; +import org.mozilla.gecko.util.StringUtils; + +public class testBookmarksPanel extends AboutHomeTest { + public void testBookmarksPanel() { + final String BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + JSONObject data = null; + + // Make sure our default bookmarks are loaded. + // Technically this will race with the check below. + initializeProfile(); + + // Add a mobile bookmark. + mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL); + + openAboutHomeTab(AboutHomeTabs.BOOKMARKS); + + // Check that the default bookmarks are displayed. + // We need to wait for the distribution to have been processed + // before this will succeed. + for (String url : mStringHelper.DEFAULT_BOOKMARKS_URLS) { + isBookmarkDisplayed(url); + } + + assertAllContextMenuOptionsArePresent(mStringHelper.DEFAULT_BOOKMARKS_URLS[1], + mStringHelper.DEFAULT_BOOKMARKS_URLS[0]); + + openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]); + + // Test that "Open in New Tab" works + final Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter); + final int tabCountInt = Integer.parseInt(tabCount.getText()); + Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added"); + mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[0]); + try { + data = new JSONObject(tabEventExpecter.blockForEventData()); + } catch (JSONException e) { + mAsserter.ok(false, "exception getting event data", e.toString()); + } + tabEventExpecter.unregisterListener(); + mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed"); + // extra check here on the Tab:Added message to be sure the right tab opened + int tabID = 0; + try { + mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri"); + tabID = data.getInt("tabID"); + } catch (JSONException e) { + mAsserter.ok(false, "exception accessing event data", e.toString()); + } + // close tab so about:firefox can be selected again + closeTab(tabID); + + // Test that "Open in Private Tab" works + openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]); + tabEventExpecter = mActions.expectGeckoEvent("Tab:Added"); + mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[1]); + try { + data = new JSONObject(tabEventExpecter.blockForEventData()); + } catch (JSONException e) { + mAsserter.ok(false, "exception getting event data", e.toString()); + } + tabEventExpecter.unregisterListener(); + mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed"); + // extra check here on the Tab:Added message to be sure the right tab opened, again + try { + mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri"); + } catch (JSONException e) { + mAsserter.ok(false, "exception accessing event data", e.toString()); + } + + // Test that "Edit" works + String[] editedBookmarkValues = new String[] { "New bookmark title", "www.NewBookmark.url", "newBookmarkKeyword" }; + editBookmark(BOOKMARK_URL, editedBookmarkValues); + checkBookmarkEdit(editedBookmarkValues[1], editedBookmarkValues); + + // Test that "Remove" works + openBookmarkContextMenu(editedBookmarkValues[1]); + mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[5]); + waitForText(mStringHelper.BOOKMARK_REMOVED_LABEL); + mAsserter.ok(!mDatabaseHelper.isBookmark(editedBookmarkValues[1]), "Checking that the bookmark was removed", "The bookmark was removed"); + } + + /** + * Asserts that all context menu items are present on the given links. For one link, + * the context menu is expected to not have the "Share" context menu item. + * + * @param shareableURL A URL that is expected to have the "Share" context menu item + * @param nonShareableURL A URL that is expected not to have the "Share" context menu item. + */ + private void assertAllContextMenuOptionsArePresent(final String shareableURL, + final String nonShareableURL) { + mAsserter.ok(StringUtils.isShareableUrl(shareableURL), "Ensuring url is shareable", ""); + mAsserter.ok(!StringUtils.isShareableUrl(nonShareableURL), "Ensuring url is not shareable", ""); + + openBookmarkContextMenu(shareableURL); + for (String contextMenuOption : mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS) { + mAsserter.ok(mSolo.searchText(contextMenuOption), + "Checking that the context menu option is present", + contextMenuOption + " is present"); + } + + // Close the menu. + mSolo.goBack(); + + openBookmarkContextMenu(nonShareableURL); + for (String contextMenuOption : mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS) { + // This link is not shareable: skip the "Share" option. + if ("Share".equals(contextMenuOption)) { + continue; + } + + mAsserter.ok(mSolo.searchText(contextMenuOption), + "Checking that the context menu option is present", + contextMenuOption + " is present"); + } + + // The use of Solo.searchText is potentially fragile as It will only + // scroll the most recently drawn view. Works fine for now though. + mAsserter.ok(!mSolo.searchText("Share"), + "Checking that the Share option is not present", + "Share option is not present"); + + // Close the menu. + mSolo.goBack(); + } + + /** + * @param bookmarkUrl URL of the bookmark to edit + * @param values String array with the new values for all fields + */ + private void editBookmark(String bookmarkUrl, String[] values) { + openBookmarkContextMenu(bookmarkUrl); + mSolo.clickOnText(mStringHelper.CONTEXT_MENU_EDIT); + waitForText(mStringHelper.EDIT_BOOKMARK); + + // Update the fields with the new values + for (int i = 0; i < values.length; i++) { + mSolo.clearEditText(i); + mSolo.clickOnEditText(i); + mActions.sendKeys(values[i]); + } + + mSolo.clickOnButton(mStringHelper.OK); + waitForText(mStringHelper.BOOKMARK_UPDATED_LABEL); + } + + /** + * @param bookmarkUrl String with the original url + * @param values String array with the new values for all fields + */ + private void checkBookmarkEdit(String bookmarkUrl, String[] values) { + openBookmarkContextMenu(bookmarkUrl); + mSolo.clickOnText(mStringHelper.CONTEXT_MENU_EDIT); + waitForText(mStringHelper.EDIT_BOOKMARK); + + // Check the values of the fields + for (String value : values) { + mAsserter.ok(mSolo.searchText(value), "Checking that the value is correct", "The value = " + value + " is correct"); + } + + mSolo.clickOnButton("Cancel"); + waitForText(mStringHelper.BOOKMARKS_LABEL); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java new file mode 100644 index 000000000..eec5c4b33 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDatabaseHelperUpgrades.java @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; + +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import org.mozilla.gecko.db.BrowserDatabaseHelper; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +// TODO: Move to junit 3 tests, once those run in automation. There is no ui testing to do so it's a better fit. +/** + * This test runs upgrades for the databases in (robocop-assets)/browser_db_upgrade. Currently, + * (robocop-assets)=mobile/android/tests/browser/robocop/assets/. + * + * It copies the old database from the robocop assets directory into a temporary file and opens a SQLiteHelper + * on the database to verify it gets upgraded to the correct version. If there is an issue with the upgrade, + * generally a SQLiteException will be thrown and the test will fail. For example: + * android.database.sqlite.SQLiteException: duplicate column name: calculated_pages_times_rating (code 1): , while compiling: ALTER TABLE book_information ADD COLUMN calculated_pages_times_rating INTEGER; + * + * Alternative upgrade tests: + * * Robolectric 2.3+ uses a real SQLite database implementation so we could run our upgrades there. However, the + * SQLite implementation may not be the same as we run on Android. Benefits: speed & we don't need the application to + * run (and thus a valid DB of the latest version) to run these tests. + * * We could copy the current database creation code into a new test, create the database, and then try to upgrade + * it. However, the tables are empty and thus not a realistic migration plan (e.g. foreign key constraints). + * + * TO EDIT THIS TEST: + * * Copy the current version of the database into (robocop-assets)/browser_db_upgrade/v##.db database. You can do + * this via Margaret's copy profile addon - take browser.db from the profile directory. This db copy should contain a + * used profile - e.g. history items, bookmarks. A good way to get a used profile is to sign into sync. + * * MAKE SURE YOU COPY YOUR DB FIRST. Then make your changes to the DB schema code. + * * Test! + * * Note: when the application starts for testing, it may need to upgrade the database from your existing version. If + * this fails, the application will crash and the test may fail to start. + * + * IMPORTANT: + * Test DBs must be created on the oldest version of Android that is currently supported. SQLite + * is not forwards compatible. E.g. uploading a DB created on a 6.0 device will cause failures + * when robocop tests running on 4.3 are unable to load it. + * + * Implementation inspired by: + * http://riggaroo.co.za/automated-testing-sqlite-database-upgrades-android/ + */ +public class testBrowserDatabaseHelperUpgrades extends UITest { + private static final int TEST_FROM_VERSION = 27; // We only started testing on this version. + + private ArrayList<File> temporaryDbFiles; + + @Override + public void setUp() throws Exception { + super.setUp(); + // TODO: We already install & remove the profile directory each run so it'd be safer for clean-up to store + // this there. That being said, temporary files are still stored in the application directory so these temporary + // files will get cleaned up when the application is uninstalled or when data is cleared. + temporaryDbFiles = new ArrayList<>(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + for (final File dbFile : temporaryDbFiles) { + dbFile.delete(); + } + } + + /** + * @throws IOException if the database cannot be copied. + */ + public void test() throws IOException { + for (int i = TEST_FROM_VERSION; i < BrowserDatabaseHelper.DATABASE_VERSION; ++i) { + Log.d(LOGTAG, "Testing upgrade from version: " + i); + final String tempDbPath = copyDatabase(i); + + final SQLiteDatabase db = SQLiteDatabase.openDatabase(tempDbPath, null, 0); + try { + fAssertEquals("Input DB isn't the expected version", + i, db.getVersion()); + } finally { + db.close(); + } + + final BrowserDatabaseHelper dbHelperToUpgrade = new BrowserDatabaseHelper(getActivity(), tempDbPath); + // Ideally, we'd test upgrading version i to version i + 1 but this method does not permit that. Alas! + final SQLiteDatabase upgradedDb = dbHelperToUpgrade.getWritableDatabase(); + try { + fAssertEquals("DB helper should upgrade to latest version", + BrowserDatabaseHelper.DATABASE_VERSION, upgradedDb.getVersion()); + } finally { + upgradedDb.close(); + } + } + } + + /** + * Copies the database from the assets directory to a temporary test file. + * + * @param version version of the database to copy. + * @return the String path to the new copy of the database + * @throws IOException if reading the existing database or writing the temporary database fails + */ + private String copyDatabase(final int version) throws IOException { + final InputStream inputStream = openDbFromAssets(version); + try { + final File dbDestination = File.createTempFile("temporaryDB-v" + version + "_", "db"); + temporaryDbFiles.add(dbDestination); + Log.d(LOGTAG, "Moving DB from assets to " + dbDestination.getPath()); + + final OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(dbDestination)); + try { + final byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, len); + } + outputStream.flush(); + } finally { + outputStream.close(); + } + + return dbDestination.getPath(); + } finally { + inputStream.close(); + } + } + + private InputStream openDbFromAssets(final int version) throws IOException { + final String dbAssetPath = String.format("browser_db_upgrade" + File.separator + String.format("v%d.db", version)); + Log.d(LOGTAG, "Opening DB from assets: " + dbAssetPath); + try { + return new BufferedInputStream(getInstrumentation().getContext().getAssets().open(dbAssetPath)); + } catch (final FileNotFoundException e) { + throw new IllegalStateException("If you're upgrading the browser.db version, " + + "you need to provide an old version of the database for this test! See the javadoc.", e); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java new file mode 100644 index 000000000..2dab2996c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserDiscovery.java @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + + + +public class testBrowserDiscovery extends JavascriptTest { + public testBrowserDiscovery() { + super("testBrowserDiscovery.js"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java new file mode 100644 index 000000000..e0ebb5c8e --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserProvider.java @@ -0,0 +1,1921 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.UrlAnnotations.SyncStatus; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.URLMetadata; +import org.mozilla.gecko.db.URLMetadataTable; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.util.Log; + +/* + * This test is meant to exercise all operations exposed by Fennec's + * history and bookmarks content provider. It does so in an isolated + * environment (see ContentProviderTest) without affecting any UI-related + * code. + */ +public class testBrowserProvider extends ContentProviderTest { + private long mMobileFolderId; + + private void loadMobileFolderId() throws Exception { + Cursor c = null; + try { + c = getBookmarkByGuid(BrowserContract.Bookmarks.MOBILE_FOLDER_GUID); + mAsserter.is(c.moveToFirst(), true, "Mobile bookmarks folder is present"); + + mMobileFolderId = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks._ID)); + } finally { + if (c != null) { + c.close(); + } + } + } + + private void ensureEmptyDatabase() throws Exception { + Cursor c; + + String guid = BrowserContract.Bookmarks.GUID; + + mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), + guid + " != ? AND " + + guid + " != ? AND " + + guid + " != ? AND " + + guid + " != ? AND " + + guid + " != ? AND " + + guid + " != ?", + new String[] { BrowserContract.Bookmarks.PLACES_FOLDER_GUID, + BrowserContract.Bookmarks.MOBILE_FOLDER_GUID, + BrowserContract.Bookmarks.MENU_FOLDER_GUID, + BrowserContract.Bookmarks.TAGS_FOLDER_GUID, + BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID, + BrowserContract.Bookmarks.UNFILED_FOLDER_GUID }); + + c = mProvider.query(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null); + assertCountIsAndClose(c, 6, "All non-special bookmarks and folders were deleted"); + + mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null); + c = mProvider.query(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null); + assertCountIsAndClose(c, 0, "All history entries were deleted"); + + /** + * There's no reason why the following two parts should fail. + * But sometimes they do, and I'm not going to spend the time + * to figure out why in an unrelated bug. + */ + + mProvider.delete(appendUriParam(BrowserContract.Favicons.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null); + c = mProvider.query(appendUriParam(BrowserContract.Favicons.CONTENT_URI, + BrowserContract.PARAM_SHOW_DELETED, "1"), + null, null, null, null); + + if (c.getCount() > 0) { + mAsserter.dumpLog("Unexpected favicons in ensureEmptyDatabase."); + } + c.close(); + + mAsserter.dumpLog("ensureEmptyDatabase: Favicon deletion completed."); // Bug 968951 debug. + // assertCountIsAndClose(c, 0, "All favicons were deleted"); + + mProvider.delete(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null); + c = mProvider.query(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI, + BrowserContract.PARAM_SHOW_DELETED, "1"), + null, null, null, null); + + if (c.getCount() > 0) { + mAsserter.dumpLog("Unexpected thumbnails in ensureEmptyDatabase."); + } + c.close(); + + mAsserter.dumpLog("ensureEmptyDatabase: Thumbnail deletion completed."); // Bug 968951 debug. + // assertCountIsAndClose(c, 0, "All thumbnails were deleted"); + } + + private ContentValues createBookmark(String title, String url, long parentId, + int type, int position, String tags, String description, String keyword) throws Exception { + ContentValues bookmark = new ContentValues(); + + bookmark.put(BrowserContract.Bookmarks.TITLE, title); + bookmark.put(BrowserContract.Bookmarks.URL, url); + bookmark.put(BrowserContract.Bookmarks.PARENT, parentId); + bookmark.put(BrowserContract.Bookmarks.TYPE, type); + bookmark.put(BrowserContract.Bookmarks.POSITION, position); + bookmark.put(BrowserContract.Bookmarks.TAGS, tags); + bookmark.put(BrowserContract.Bookmarks.DESCRIPTION, description); + bookmark.put(BrowserContract.Bookmarks.KEYWORD, keyword); + + return bookmark; + } + + private ContentValues createOneBookmark() throws Exception { + return createBookmark("Example", "http://example.com", mMobileFolderId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + } + + private Cursor getBookmarksByParent(long parent) throws Exception { + // Order by position. + return mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null, + BrowserContract.Bookmarks.PARENT + " = ?", + new String[] { String.valueOf(parent) }, + BrowserContract.Bookmarks.POSITION); + } + + private Cursor getBookmarkByGuid(String guid) throws Exception { + return mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null, + BrowserContract.Bookmarks.GUID + " = ?", + new String[] { guid }, + null); + } + + private Cursor getBookmarkById(long id) throws Exception { + return getBookmarkById(BrowserContract.Bookmarks.CONTENT_URI, id, null); + } + + private Cursor getBookmarkById(long id, String[] projection) throws Exception { + return getBookmarkById(BrowserContract.Bookmarks.CONTENT_URI, id, projection); + } + + private Cursor getBookmarkById(Uri bookmarksUri, long id) throws Exception { + return getBookmarkById(bookmarksUri, id, null); + } + + private Cursor getBookmarkById(Uri bookmarksUri, long id, String[] projection) throws Exception { + return mProvider.query(bookmarksUri, projection, + BrowserContract.Bookmarks._ID + " = ?", + new String[] { String.valueOf(id) }, + null); + } + + private ContentValues createHistoryEntry(String title, String url, int visits, long lastVisited) throws Exception { + ContentValues historyEntry = new ContentValues(); + + historyEntry.put(BrowserContract.History.TITLE, title); + historyEntry.put(BrowserContract.History.URL, url); + historyEntry.put(BrowserContract.History.VISITS, visits); + historyEntry.put(BrowserContract.History.DATE_LAST_VISITED, lastVisited); + + return historyEntry; + } + + private ContentValues createFaviconEntry(String pageUrl, String data) throws Exception { + ContentValues faviconEntry = new ContentValues(); + + faviconEntry.put(BrowserContract.Favicons.PAGE_URL, pageUrl); + faviconEntry.put(BrowserContract.Favicons.URL, pageUrl + "/favicon.ico"); + faviconEntry.put(BrowserContract.Favicons.DATA, data.getBytes("UTF8")); + + return faviconEntry; + } + + private ContentValues createThumbnailEntry(String pageUrl, String data) throws Exception { + ContentValues thumbnailEntry = new ContentValues(); + + thumbnailEntry.put(BrowserContract.Thumbnails.URL, pageUrl); + thumbnailEntry.put(BrowserContract.Thumbnails.DATA, data.getBytes("UTF8")); + + return thumbnailEntry; + } + + private ContentValues createUrlMetadataEntry(final String url, final String tileImage, final String tileColor, + final String touchIcon) { + final ContentValues values = new ContentValues(); + values.put(URLMetadataTable.URL_COLUMN, url); + values.put(URLMetadataTable.TILE_IMAGE_URL_COLUMN, tileImage); + values.put(URLMetadataTable.TILE_COLOR_COLUMN, tileColor); + values.put(URLMetadataTable.TOUCH_ICON_COLUMN, touchIcon); + return values; + } + + private ContentValues createUrlAnnotationEntry(final String url, final String key, final String value, + final long dateCreated) { + final ContentValues values = new ContentValues(); + values.put(BrowserContract.UrlAnnotations.URL, url); + values.put(BrowserContract.UrlAnnotations.KEY, key); + values.put(BrowserContract.UrlAnnotations.VALUE, value); + values.put(BrowserContract.UrlAnnotations.DATE_CREATED, dateCreated); + values.put(BrowserContract.UrlAnnotations.DATE_MODIFIED, dateCreated); + return values; + } + + private ContentValues createOneHistoryEntry() throws Exception { + return createHistoryEntry("Example", "http://example.com", 10, System.currentTimeMillis()); + } + + private Cursor getHistoryEntryById(long id) throws Exception { + return getHistoryEntryById(BrowserContract.History.CONTENT_URI, id, null); + } + + private Cursor getHistoryEntryById(long id, String[] projection) throws Exception { + return getHistoryEntryById(BrowserContract.History.CONTENT_URI, id, projection); + } + + private Cursor getHistoryEntryById(Uri historyUri, long id) throws Exception { + return getHistoryEntryById(historyUri, id, null); + } + + private Cursor getHistoryEntryById(Uri historyUri, long id, String[] projection) throws Exception { + return mProvider.query(historyUri, projection, + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }, + null); + } + + private Cursor getFaviconsByUrl(String url) throws Exception { + return mProvider.query(BrowserContract.Combined.CONTENT_URI, null, + BrowserContract.Combined.URL + " = ?", + new String[] { url }, + null); + } + + private Cursor getThumbnailByUrl(String url) throws Exception { + return mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, + BrowserContract.Thumbnails.URL + " = ?", + new String[] { url }, + null); + } + + private Cursor getUrlAnnotationByUrl(final String url) throws Exception { + return mProvider.query(BrowserContract.UrlAnnotations.CONTENT_URI, null, + BrowserContract.UrlAnnotations.URL + " = ?", + new String[] { url }, + null); + } + + private Cursor getUrlMetadataByUrl(final String url) throws Exception { + return mProvider.query(URLMetadataTable.CONTENT_URI, null, + URLMetadataTable.URL_COLUMN + " = ?", + new String[] { url }, + null); + } + + @Override + public void setUp() throws Exception { + super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db"); + + mTests.add(new TestSpecialFolders()); + + mTests.add(new TestInsertBookmarks()); + mTests.add(new TestInsertBookmarksFavicons()); + mTests.add(new TestDeleteBookmarks()); + mTests.add(new TestDeleteBookmarksFavicons()); + mTests.add(new TestUpdateBookmarks()); + mTests.add(new TestUpdateBookmarksFavicons()); + mTests.add(new TestPositionBookmarks()); + + mTests.add(new TestInsertHistory()); + mTests.add(new TestInsertHistoryFavicons()); + mTests.add(new TestDeleteHistory()); + mTests.add(new TestDeleteHistoryFavicons()); + mTests.add(new TestUpdateHistory()); + mTests.add(new TestUpdateHistoryFavicons()); + mTests.add(new TestUpdateOrInsertHistory()); + mTests.add(new TestInsertHistoryThumbnails()); + mTests.add(new TestUpdateHistoryThumbnails()); + mTests.add(new TestDeleteHistoryThumbnails()); + + mTests.add(new TestInsertUrlAnnotations()); + mTests.add(new TestInsertUrlMetadata()); + + mTests.add(new TestBatchOperations()); + + mTests.add(new TestCombinedView()); + mTests.add(new TestCombinedViewDisplay()); + mTests.add(new TestCombinedViewWithDeletedBookmark()); + + mTests.add(new TestBrowserProviderNotifications()); + } + + public void testBrowserProvider() throws Exception { + loadMobileFolderId(); + + for (int i = 0; i < mTests.size(); i++) { + Runnable test = mTests.get(i); + + final String testName = test.getClass().getSimpleName(); + setTestName(testName); + ensureEmptyDatabase(); + mAsserter.dumpLog("testBrowserProvider: Database empty - Starting " + testName + "."); + test.run(); + } + } + + private class TestBatchOperations extends TestCase { + static final int TESTCOUNT = 100; + + public void testApplyBatch() throws Exception { + ArrayList<ContentProviderOperation> mOperations + = new ArrayList<ContentProviderOperation>(); + + // Test a bunch of inserts with applyBatch + ContentValues values = new ContentValues(); + ContentProviderOperation.Builder builder = null; + + for (int i = 0; i < TESTCOUNT; i++) { + values.clear(); + values.put(BrowserContract.History.VISITS, i); + values.put(BrowserContract.History.TITLE, "Test" + i); + values.put(BrowserContract.History.URL, "http://www.test.org/" + i); + + // Insert + builder = ContentProviderOperation.newInsert(BrowserContract.History.CONTENT_URI); + builder.withValues(values); + // Queue the operation + mOperations.add(builder.build()); + } + + ContentProviderResult[] applyResult = + mProvider.applyBatch(mOperations); + + boolean allFound = true; + for (int i = 0; i < TESTCOUNT; i++) { + Cursor cursor = mProvider.query(BrowserContract.History.CONTENT_URI, + null, + BrowserContract.History.URL + " = ?", + new String[] { "http://www.test.org/" + i }, + null); + + if (!cursor.moveToFirst()) + allFound = false; + cursor.close(); + } + mAsserter.is(allFound, true, "Found all batchApply entries"); + mOperations.clear(); + + // Update all visits to 1 + values.clear(); + values.put(BrowserContract.History.VISITS, 1); + for (int i = 0; i < TESTCOUNT; i++) { + builder = ContentProviderOperation.newUpdate(BrowserContract.History.CONTENT_URI); + builder.withSelection(BrowserContract.History.URL + " = ?", + new String[] {"http://www.test.org/" + i}); + builder.withValues(values); + builder.withExpectedCount(1); + // Queue the operation + mOperations.add(builder.build()); + } + + boolean seenException = false; + try { + applyResult = mProvider.applyBatch(mOperations); + } catch (OperationApplicationException ex) { + seenException = true; + } + mAsserter.is(seenException, false, "Batch updating succeeded"); + mOperations.clear(); + + // Delete all visits + for (int i = 0; i < TESTCOUNT; i++) { + builder = ContentProviderOperation.newDelete(BrowserContract.History.CONTENT_URI); + builder.withSelection(BrowserContract.History.URL + " = ?", + new String[] {"http://www.test.org/" + i}); + builder.withExpectedCount(1); + // Queue the operation + mOperations.add(builder.build()); + } + try { + applyResult = mProvider.applyBatch(mOperations); + } catch (OperationApplicationException ex) { + seenException = true; + } + mAsserter.is(seenException, false, "Batch deletion succeeded"); + } + + // Force a Constraint error, see if later operations still apply correctly + public void testApplyBatchErrors() throws Exception { + ArrayList<ContentProviderOperation> mOperations + = new ArrayList<ContentProviderOperation>(); + + // Test a bunch of inserts with applyBatch + ContentProviderOperation.Builder builder = null; + ContentValues values = createFaviconEntry("http://www.test.org", "FAVICON"); + builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI); + builder.withValues(values); + mOperations.add(builder.build()); + + // Make a duplicate, this will fail because of a UNIQUE constraint + builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI); + builder.withValues(values); + mOperations.add(builder.build()); + + // This is valid and should be in the table afterwards + values.put(BrowserContract.Favicons.URL, "http://www.test.org/valid.ico"); + builder = ContentProviderOperation.newInsert(BrowserContract.Favicons.CONTENT_URI); + builder.withValues(values); + mOperations.add(builder.build()); + + boolean seenException = false; + + try { + ContentProviderResult[] applyResult = + mProvider.applyBatch(mOperations); + } catch (OperationApplicationException ex) { + seenException = true; + } + + // This test may need to go away if Bug 717428 is fixed. + mAsserter.is(seenException, true, "Expected failure in favicons table"); + + boolean allFound = true; + Cursor cursor = mProvider.query(BrowserContract.Favicons.CONTENT_URI, + null, + BrowserContract.Favicons.URL + " = ?", + new String[] { "http://www.test.org/valid.ico" }, + null); + + if (!cursor.moveToFirst()) + allFound = false; + cursor.close(); + + mAsserter.is(allFound, true, "Found all applyBatch (with error) entries"); + } + + public void testBulkInsert() throws Exception { + // Test a bunch of inserts with bulkInsert + ContentValues allVals[] = new ContentValues[TESTCOUNT]; + for (int i = 0; i < TESTCOUNT; i++) { + allVals[i] = new ContentValues(); + allVals[i].put(BrowserContract.History.URL, i); + allVals[i].put(BrowserContract.History.TITLE, "Test" + i); + allVals[i].put(BrowserContract.History.URL, "http://www.test.org/" + i); + } + + int inserts = mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, allVals); + mAsserter.is(inserts, TESTCOUNT, "Excepted number of inserts matches"); + + boolean allFound = true; + for (int i = 0; i < TESTCOUNT; i++) { + Cursor cursor = mProvider.query(BrowserContract.History.CONTENT_URI, + null, + BrowserContract.History.URL + " = ?", + new String[] { "http://www.test.org/" + i }, + null); + + if (!cursor.moveToFirst()) + allFound = false; + cursor.close(); + } + mAsserter.is(allFound, true, "Found all bulkInsert entries"); + } + + @Override + public void test() throws Exception { + testApplyBatch(); + // Clean up + ensureEmptyDatabase(); + + testBulkInsert(); + ensureEmptyDatabase(); + + testApplyBatchErrors(); + } + } + + private class TestSpecialFolders extends TestCase { + @Override + public void test() throws Exception { + Cursor c = mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, + new String[] { BrowserContract.Bookmarks._ID, + BrowserContract.Bookmarks.GUID, + BrowserContract.Bookmarks.PARENT }, + BrowserContract.Bookmarks.GUID + " = ? OR " + + BrowserContract.Bookmarks.GUID + " = ? OR " + + BrowserContract.Bookmarks.GUID + " = ? OR " + + BrowserContract.Bookmarks.GUID + " = ? OR " + + BrowserContract.Bookmarks.GUID + " = ? OR " + + BrowserContract.Bookmarks.GUID + " = ?", + new String[] { BrowserContract.Bookmarks.PLACES_FOLDER_GUID, + BrowserContract.Bookmarks.MOBILE_FOLDER_GUID, + BrowserContract.Bookmarks.MENU_FOLDER_GUID, + BrowserContract.Bookmarks.TAGS_FOLDER_GUID, + BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID, + BrowserContract.Bookmarks.UNFILED_FOLDER_GUID}, + null); + + mAsserter.is(c.getCount(), 6, "Right number of special folders"); + + int rootId = BrowserContract.Bookmarks.FIXED_ROOT_ID; + + while (c.moveToNext()) { + int id = c.getInt(c.getColumnIndex(BrowserContract.Bookmarks._ID)); + String guid = c.getString(c.getColumnIndex(BrowserContract.Bookmarks.GUID)); + int parentId = c.getInt(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)); + + if (guid.equals(BrowserContract.Bookmarks.PLACES_FOLDER_GUID)) { + mAsserter.is(id, rootId, "The id of places folder is correct"); + } + + mAsserter.is(parentId, rootId, + "The PARENT of the " + guid + " special folder is correct"); + } + + c.close(); + } + } + + private class TestInsertBookmarks extends TestCase { + private long insertWithNullCol(String colName) throws Exception { + ContentValues b = createOneBookmark(); + b.putNull(colName); + long id = -1; + + try { + id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + } catch (Exception e) {} + + return id; + } + + @Override + public void test() throws Exception { + ContentValues b = createOneBookmark(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + + final Cursor c = getBookmarkById(id); + try { + mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), b.getAsString(BrowserContract.Bookmarks.TITLE), + "Inserted bookmark has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), b.getAsString(BrowserContract.Bookmarks.URL), + "Inserted bookmark has correct URL"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), b.getAsString(BrowserContract.Bookmarks.TAGS), + "Inserted bookmark has correct tags"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), b.getAsString(BrowserContract.Bookmarks.KEYWORD), + "Inserted bookmark has correct keyword"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), b.getAsString(BrowserContract.Bookmarks.DESCRIPTION), + "Inserted bookmark has correct description"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), b.getAsString(BrowserContract.Bookmarks.POSITION), + "Inserted bookmark has correct position"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), b.getAsString(BrowserContract.Bookmarks.TYPE), + "Inserted bookmark has correct type"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)), b.getAsString(BrowserContract.Bookmarks.PARENT), + "Inserted bookmark has correct parent ID"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.IS_DELETED)), String.valueOf(0), + "Inserted bookmark has correct is-deleted state"); + + id = insertWithNullCol(BrowserContract.Bookmarks.POSITION); + mAsserter.is(id, -1L, + "Should not be able to insert bookmark with null position"); + + id = insertWithNullCol(BrowserContract.Bookmarks.TYPE); + mAsserter.is(id, -1L, + "Should not be able to insert bookmark with null type"); + + if (Build.VERSION.SDK_INT >= 8 && + Build.VERSION.SDK_INT < 16) { + b = createOneBookmark(); + b.put(BrowserContract.Bookmarks.PARENT, -1); + id = -1; + + try { + id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + } catch (Exception e) {} + + mAsserter.is(id, -1L, + "Should not be able to insert bookmark with invalid parent"); + } + + b = createOneBookmark(); + b.remove(BrowserContract.Bookmarks.TYPE); + id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + final Cursor c2 = getBookmarkById(id); + try { + mAsserter.is(c2.moveToFirst(), true, "Inserted bookmark found"); + mAsserter.is(c2.getString(c2.getColumnIndex(BrowserContract.Bookmarks.TYPE)), String.valueOf(BrowserContract.Bookmarks.TYPE_BOOKMARK), + "Inserted bookmark has correct default type"); + } finally { + c2.close(); + } + } finally { + c.close(); + } + } + } + + private class TestInsertBookmarksFavicons extends TestCase { + @Override + public void test() throws Exception { + ContentValues b = createOneBookmark(); + + final String favicon = "FAVICON"; + final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL); + + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + + // Insert the favicon into the favicons table + mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon)); + + Cursor c = getBookmarkById(id, new String[] { BrowserContract.Bookmarks.FAVICON }); + + mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON)), "UTF8"), + favicon, "Inserted bookmark has corresponding favicon image"); + c.close(); + + c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted favicon found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"), + favicon, "Inserted favicon has corresponding favicon image"); + c.close(); + } + } + + private class TestDeleteBookmarks extends TestCase { + private long insertOneBookmark() throws Exception { + ContentValues b = createOneBookmark(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + + Cursor c = getBookmarkById(id); + mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found"); + c.close(); + + return id; + } + + @Override + public void test() throws Exception { + long id = insertOneBookmark(); + + int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI, + BrowserContract.Bookmarks._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted"); + + Cursor c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id); + mAsserter.is(c.moveToFirst(), true, "Deleted bookmark was only marked as deleted"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), null, + "Deleted bookmark title is null"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), null, + "Deleted bookmark URL is null"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), null, + "Deleted bookmark tags is null"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), null, + "Deleted bookmark keyword is null"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), null, + "Deleted bookmark description is null"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), String.valueOf(0), + "Deleted bookmark has correct position"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.PARENT)), null, + "Deleted bookmark parent ID is null"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.IS_DELETED)), String.valueOf(1), + "Deleted bookmark has correct is-deleted state"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON_ID)), null, + "Deleted bookmark Favicon ID is null"); + mAsserter.isnot(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.GUID)), null, + "Deleted bookmark GUID is not null"); + c.close(); + + deleted = mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), + BrowserContract.Bookmarks._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted"); + + c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id); + mAsserter.is(c.moveToFirst(), false, "Inserted bookmark is now actually deleted"); + c.close(); + + id = insertOneBookmark(); + + deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null); + mAsserter.is((deleted == 1), true, + "Inserted bookmark was deleted using URI with id"); + + c = getBookmarkById(id); + mAsserter.is(c.moveToFirst(), false, + "Inserted bookmark can't be found after deletion using URI with ID"); + c.close(); + + if (Build.VERSION.SDK_INT >= 8 && + Build.VERSION.SDK_INT < 16) { + ContentValues b = createBookmark("Folder", null, mMobileFolderId, + BrowserContract.Bookmarks.TYPE_FOLDER, 0, "folderTags", "folderDescription", "folderKeyword"); + + long parentId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + c = getBookmarkById(parentId); + mAsserter.is(c.moveToFirst(), true, "Inserted bookmarks folder found"); + c.close(); + + b = createBookmark("Example", "http://example.com", parentId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + + id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + c = getBookmarkById(id); + mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found"); + c.close(); + + deleted = 0; + try { + Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, parentId); + deleted = mProvider.delete(appendUriParam(uri, BrowserContract.PARAM_IS_SYNC, "1"), null, null); + } catch(Exception e) {} + + mAsserter.is((deleted == 0), true, + "Should not be able to delete folder that causes orphan bookmarks"); + } + } + } + + private class TestDeleteBookmarksFavicons extends TestCase { + @Override + public void test() throws Exception { + ContentValues b = createOneBookmark(); + + final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + + // Insert the favicon into the favicons table + mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON")); + + Cursor c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted favicon found"); + c.close(); + + mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null); + + c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it"); + c.close(); + } + } + + private class TestUpdateBookmarks extends TestCase { + private int updateWithNullCol(long id, String colName) throws Exception { + ContentValues u = new ContentValues(); + u.putNull(colName); + + int updated = 0; + + try { + updated = mProvider.update(BrowserContract.Bookmarks.CONTENT_URI, u, + BrowserContract.Bookmarks._ID + " = ?", + new String[] { String.valueOf(id) }); + } catch (Exception e) {} + + return updated; + } + + @Override + public void test() throws Exception { + ContentValues b = createOneBookmark(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b)); + + Cursor c = getBookmarkById(id); + mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found"); + + long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_CREATED)); + long dateModified = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_MODIFIED)); + + ContentValues u = new ContentValues(); + u.put(BrowserContract.Bookmarks.TITLE, b.getAsString(BrowserContract.Bookmarks.TITLE) + "CHANGED"); + u.put(BrowserContract.Bookmarks.URL, b.getAsString(BrowserContract.Bookmarks.URL) + "/more/stuff"); + u.put(BrowserContract.Bookmarks.TAGS, b.getAsString(BrowserContract.Bookmarks.TAGS) + "CHANGED"); + u.put(BrowserContract.Bookmarks.DESCRIPTION, b.getAsString(BrowserContract.Bookmarks.DESCRIPTION) + "CHANGED"); + u.put(BrowserContract.Bookmarks.KEYWORD, b.getAsString(BrowserContract.Bookmarks.KEYWORD) + "CHANGED"); + u.put(BrowserContract.Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_FOLDER); + u.put(BrowserContract.Bookmarks.POSITION, 10); + + int updated = mProvider.update(BrowserContract.Bookmarks.CONTENT_URI, u, + BrowserContract.Bookmarks._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is((updated == 1), true, "Inserted bookmark was updated"); + c.close(); + + c = getBookmarkById(id); + mAsserter.is(c.moveToFirst(), true, "Updated bookmark found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), u.getAsString(BrowserContract.Bookmarks.TITLE), + "Inserted bookmark has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL), + "Inserted bookmark has correct URL"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TAGS)), u.getAsString(BrowserContract.Bookmarks.TAGS), + "Inserted bookmark has correct tags"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.KEYWORD)), u.getAsString(BrowserContract.Bookmarks.KEYWORD), + "Inserted bookmark has correct keyword"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.DESCRIPTION)), u.getAsString(BrowserContract.Bookmarks.DESCRIPTION), + "Inserted bookmark has correct description"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.POSITION)), u.getAsString(BrowserContract.Bookmarks.POSITION), + "Inserted bookmark has correct position"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), u.getAsString(BrowserContract.Bookmarks.TYPE), + "Inserted bookmark has correct type"); + + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_CREATED)), + dateCreated, + "Updated bookmark has same creation date"); + + mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.Bookmarks.DATE_MODIFIED)), + dateModified, + "Updated bookmark has new modification date"); + + updated = updateWithNullCol(id, BrowserContract.Bookmarks.POSITION); + mAsserter.is((updated > 0), false, + "Should not be able to update bookmark with null position"); + + updated = updateWithNullCol(id, BrowserContract.Bookmarks.TYPE); + mAsserter.is((updated > 0), false, + "Should not be able to update bookmark with null type"); + + u = new ContentValues(); + u.put(BrowserContract.Bookmarks.URL, "http://examples2.com"); + + updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), u, null, null); + c.close(); + + c = getBookmarkById(id); + mAsserter.is(c.moveToFirst(), true, "Updated bookmark found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL), + "Updated bookmark has correct URL using URI with id"); + c.close(); + } + } + + private class TestUpdateBookmarksFavicons extends TestCase { + @Override + public void test() throws Exception { + ContentValues b = createOneBookmark(); + + final String favicon = "FAVICON"; + final String newFavicon = "NEW_FAVICON"; + final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL); + + mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b); + + // Insert the favicon into the favicons table + ContentValues f = createFaviconEntry(pageUrl, favicon); + long faviconId = ContentUris.parseId(mProvider.insert(BrowserContract.Favicons.CONTENT_URI, f)); + + Cursor c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted favicon found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"), + favicon, "Inserted favicon has corresponding favicon image"); + + ContentValues u = createFaviconEntry(pageUrl, newFavicon); + mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null); + c.close(); + + c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Updated favicon found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"), + newFavicon, "Updated favicon has corresponding favicon image"); + c.close(); + } + } + + /** + * Create a folder of one thousand and one bookmarks, then impose an order + * on them. + * + * Verify that the reordering worked by querying. + */ + private class TestPositionBookmarks extends TestCase { + + public String makeGUID(final long in) { + String part = String.valueOf(in); + return "aaaaaaaaaaaa".substring(0, (12 - part.length())) + part; + } + + public void compareCursorToItems(final Cursor c, final String[] items, final int count) { + mAsserter.is(c.moveToFirst(), true, "Folder has children."); + + int posColumn = c.getColumnIndex(BrowserContract.Bookmarks.POSITION); + int guidColumn = c.getColumnIndex(BrowserContract.Bookmarks.GUID); + int i = 0; + + while (!c.isAfterLast()) { + String guid = c.getString(guidColumn); + long pos = c.getLong(posColumn); + if ((pos != i) || (guid == null) || (!guid.equals(items[i]))) { + mAsserter.is(pos, (long) i, "Position matches sequence."); + mAsserter.is(guid, items[i], "GUID matches sequence."); + } + ++i; + c.moveToNext(); + } + + mAsserter.is(i, count, "Folder has the right number of children."); + c.close(); + } + + public static final int NUMBER_OF_CHILDREN = 1001; + @Override + public void test() throws Exception { + // Create the containing folder. + ContentValues folder = createBookmark("FolderFolder", "", mMobileFolderId, + BrowserContract.Bookmarks.TYPE_FOLDER, 0, "", + "description", "keyword"); + folder.put(BrowserContract.Bookmarks.GUID, "folderfolder"); + long folderId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, folder)); + + mAsserter.dumpLog("TestPositionBookmarks: Folder inserted"); // Bug 968951 debug. + + // Create the children. + String[] items = new String[NUMBER_OF_CHILDREN]; + + // Reuse the same ContentValues. + ContentValues item = createBookmark("Test Bookmark", "http://example.com", folderId, + BrowserContract.Bookmarks.TYPE_FOLDER, 0, "", + "description", "keyword"); + + for (int i = 0; i < NUMBER_OF_CHILDREN; ++i) { + String guid = makeGUID(i); + items[i] = guid; + item.put(BrowserContract.Bookmarks.GUID, guid); + item.put(BrowserContract.Bookmarks.POSITION, i); + item.put(BrowserContract.Bookmarks.URL, "http://example.com/" + guid); + item.put(BrowserContract.Bookmarks.TITLE, "Test Bookmark " + guid); + mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, item); + } + + mAsserter.dumpLog("TestPositionBookmarks: Bookmarks inserted"); // Bug 968951 debug. + + Cursor c; + + // Verify insertion. + c = getBookmarksByParent(folderId); + mAsserter.dumpLog("TestPositionBookmarks: Got bookmarks by parent"); // Bug 968951 debug. + compareCursorToItems(c, items, NUMBER_OF_CHILDREN); + c.close(); + + // Now permute the items array. + Random rand = new Random(); + for (int i = 0; i < NUMBER_OF_CHILDREN; ++i) { + final int newPosition = rand.nextInt(NUMBER_OF_CHILDREN); + final String switched = items[newPosition]; + items[newPosition] = items[i]; + items[i] = switched; + } + + // Impose the positions. + long updated = mProvider.update(BrowserContract.Bookmarks.POSITIONS_CONTENT_URI, null, null, items); + mAsserter.is(updated, (long) NUMBER_OF_CHILDREN, "Updated " + NUMBER_OF_CHILDREN + " positions."); + + // Verify that the database was updated. + c = getBookmarksByParent(folderId); + compareCursorToItems(c, items, NUMBER_OF_CHILDREN); + c.close(); + } + } + + private class TestInsertHistory extends TestCase { + private long insertWithNullCol(String colName) throws Exception { + ContentValues h = createOneHistoryEntry(); + h.putNull(colName); + long id = -1; + + try { + id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + } catch (Exception e) {} + + return id; + } + + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + Cursor c = getHistoryEntryById(id); + + mAsserter.is(c.moveToFirst(), true, "Inserted history entry found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), h.getAsString(BrowserContract.History.TITLE), + "Inserted history entry has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), h.getAsString(BrowserContract.History.URL), + "Inserted history entry has correct URL"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.VISITS)), h.getAsString(BrowserContract.History.VISITS), + "Inserted history entry has correct number of visits"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)), h.getAsString(BrowserContract.History.DATE_LAST_VISITED), + "Inserted history entry has correct last visited date"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.IS_DELETED)), String.valueOf(0), + "Inserted history entry has correct is-deleted state"); + + id = insertWithNullCol(BrowserContract.History.URL); + mAsserter.is(id, -1L, + "Should not be able to insert history with null URL"); + + id = insertWithNullCol(BrowserContract.History.VISITS); + mAsserter.is(id, -1L, + "Should not be able to insert history with null number of visits"); + c.close(); + } + } + + private class TestInsertHistoryFavicons extends TestCase { + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + + final String favicon = "FAVICON"; + final String pageUrl = h.getAsString(BrowserContract.History.URL); + + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + + // Insert the favicon into the favicons table + mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon)); + + Cursor c = getHistoryEntryById(id, new String[] { BrowserContract.History.FAVICON }); + + mAsserter.is(c.moveToFirst(), true, "Inserted history entry found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.History.FAVICON)), "UTF8"), + favicon, "Inserted history entry has corresponding favicon image"); + c.close(); + + c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted favicon found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"), + favicon, "Inserted favicon has corresponding favicon image"); + c.close(); + } + } + + private class TestDeleteHistory extends TestCase { + private long insertOneHistoryEntry() throws Exception { + ContentValues h = createOneHistoryEntry(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + + Cursor c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "Inserted history entry found"); + c.close(); + + return id; + } + + @Override + public void test() throws Exception { + long id = insertOneHistoryEntry(); + + int deleted = mProvider.delete(BrowserContract.History.CONTENT_URI, + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is((deleted == 1), true, "Inserted history entry was deleted"); + + Cursor c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id); + mAsserter.is(c.moveToFirst(), true, "Deleted history entry was only marked as deleted"); + + deleted = mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is((deleted == 1), true, "Inserted history entry was deleted"); + c.close(); + + c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id); + mAsserter.is(c.moveToFirst(), false, "Inserted history is now actually deleted"); + + id = insertOneHistoryEntry(); + + deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null); + mAsserter.is((deleted == 1), true, + "Inserted history entry was deleted using URI with id"); + c.close(); + + c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), false, + "Inserted history entry can't be found after deletion using URI with ID"); + c.close(); + } + } + + private class TestDeleteHistoryFavicons extends TestCase { + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + final String pageUrl = h.getAsString(BrowserContract.History.URL); + + // Insert the favicon into the favicons table + mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON")); + + Cursor c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted favicon found"); + + mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null); + c.close(); + + c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it"); + c.close(); + } + } + + private class TestUpdateHistory extends TestCase { + private int updateWithNullCol(long id, String colName) throws Exception { + ContentValues u = new ContentValues(); + u.putNull(colName); + + int updated = 0; + + try { + updated = mProvider.update(BrowserContract.History.CONTENT_URI, u, + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }); + } catch (Exception e) {} + + return updated; + } + + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + + Cursor c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "Inserted history entry found"); + + long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)); + long dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)); + + ContentValues u = new ContentValues(); + u.put(BrowserContract.History.VISITS, h.getAsInteger(BrowserContract.History.VISITS) + 1); + u.put(BrowserContract.History.DATE_LAST_VISITED, System.currentTimeMillis()); + u.put(BrowserContract.History.TITLE, h.getAsString(BrowserContract.History.TITLE) + "CHANGED"); + u.put(BrowserContract.History.URL, h.getAsString(BrowserContract.History.URL) + "/more/stuff"); + + int updated = mProvider.update(BrowserContract.History.CONTENT_URI, u, + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is((updated == 1), true, "Inserted history entry was updated"); + c.close(); + + c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "Updated history entry found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), u.getAsString(BrowserContract.History.TITLE), + "Updated history entry has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL), + "Updated history entry has correct URL"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.VISITS)), u.getAsString(BrowserContract.History.VISITS), + "Updated history entry has correct number of visits"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)), u.getAsString(BrowserContract.History.DATE_LAST_VISITED), + "Updated history entry has correct last visited date"); + + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), + dateCreated, + "Updated history entry has same creation date"); + + mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), + dateModified, + "Updated history entry has new modification date"); + + updated = updateWithNullCol(id, BrowserContract.History.URL); + mAsserter.is((updated > 0), false, + "Should not be able to update history with null URL"); + + updated = updateWithNullCol(id, BrowserContract.History.VISITS); + mAsserter.is((updated > 0), false, + "Should not be able to update history with null number of visits"); + + u = new ContentValues(); + u.put(BrowserContract.History.URL, "http://examples2.com"); + + updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), u, null, null); + c.close(); + + c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "Updated history entry found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL), + "Updated history entry has correct URL using URI with id"); + c.close(); + } + } + + private class TestUpdateHistoryFavicons extends TestCase { + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + + final String favicon = "FAVICON"; + final String newFavicon = "NEW_FAVICON"; + final String pageUrl = h.getAsString(BrowserContract.History.URL); + + mProvider.insert(BrowserContract.History.CONTENT_URI, h); + + // Insert the favicon into the favicons table + mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon)); + + Cursor c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted favicon found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"), + favicon, "Inserted favicon has corresponding favicon image"); + + ContentValues u = createFaviconEntry(pageUrl, newFavicon); + + mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null); + c.close(); + + c = getFaviconsByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Updated favicon found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"), + newFavicon, "Updated favicon has corresponding favicon image"); + c.close(); + } + } + + private class TestUpdateOrInsertHistory extends TestCase { + private final String TEST_URL_1 = "http://example.com"; + private final String TEST_URL_2 = "http://example.org"; + private final String TEST_TITLE = "Example"; + + private long getHistoryEntryIdByUrl(String url) { + Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, + new String[] { BrowserContract.History._ID }, + BrowserContract.History.URL + " = ?", + new String[] { url }, + null); + c.moveToFirst(); + long id = c.getLong(0); + c.close(); + + return id; + } + + @Override + public void test() throws Exception { + Uri updateHistoryUri = BrowserContract.History.CONTENT_URI.buildUpon(). + appendQueryParameter("increment_visits", "true").build(); + Uri updateOrInsertHistoryUri = BrowserContract.History.CONTENT_URI.buildUpon(). + appendQueryParameter("insert_if_needed", "true"). + appendQueryParameter("increment_visits", "true").build(); + + // Update a non-existent history entry, without specifying visits or title + ContentValues values = new ContentValues(); + values.put(BrowserContract.History.URL, TEST_URL_1); + + int updated = mProvider.update(updateHistoryUri, values, + BrowserContract.History.URL + " = ?", + new String[] { TEST_URL_1 }); + mAsserter.is((updated == 0), true, "History entry was not updated"); + Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, null, null, null, null); + mAsserter.is(c.moveToFirst(), false, "History entry was not inserted"); + c.close(); + + // Now let's try with update-or-insert. + updated = mProvider.update(updateOrInsertHistoryUri, values, + BrowserContract.History.URL + " = ?", + new String[] { TEST_URL_1 }); + mAsserter.is((updated == 1), true, "History entry was inserted"); + + long id = getHistoryEntryIdByUrl(TEST_URL_1); + c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "History entry was inserted"); + + long dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)); + long dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)); + + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 1L, + "Inserted history entry has correct default number of visits"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_URL_1, + "Inserted history entry has correct default title"); + + // Update the history entry, without specifying an additional visit count + values = new ContentValues(); + values.put(BrowserContract.History.DATE_LAST_VISITED, System.currentTimeMillis()); + values.put(BrowserContract.History.TITLE, TEST_TITLE); + + updated = mProvider.update(updateOrInsertHistoryUri, values, + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }); + mAsserter.is((updated == 1), true, "Inserted history entry was updated"); + c.close(); + + c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "Updated history entry found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE, + "Updated history entry has correct title"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 2L, + "Updated history entry has correct number of visits"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), dateCreated, + "Updated history entry has same creation date"); + mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), dateModified, + "Updated history entry has new modification date"); + + // Create a new history entry, specifying visits and history + values = new ContentValues(); + values.put(BrowserContract.History.URL, TEST_URL_2); + values.put(BrowserContract.History.TITLE, TEST_TITLE); + values.put(BrowserContract.History.VISITS, 10); + + updated = mProvider.update(updateOrInsertHistoryUri, values, + BrowserContract.History.URL + " = ?", + new String[] { values.getAsString(BrowserContract.History.URL) }); + mAsserter.is((updated == 1), true, "History entry was inserted"); + + id = getHistoryEntryIdByUrl(TEST_URL_2); + c.close(); + + c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "History entry was inserted"); + + dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)); + dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)); + + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 10L, + "Inserted history entry has correct specified number of visits"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE, + "Inserted history entry has correct specified title"); + + // Update the history entry, specifying additional visit count. + // The expectation is that the value is ignored, and count is bumped by 1 only. + // At the same time, a visit is inserted into the visits table. + // See junit4 tests in BrowserProviderHistoryVisitsTest. + values = new ContentValues(); + values.put(BrowserContract.History.VISITS, 10); + + updated = mProvider.update(updateOrInsertHistoryUri, values, + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }); + mAsserter.is((updated == 1), true, "Inserted history entry was updated"); + c.close(); + + c = getHistoryEntryById(id); + mAsserter.is(c.moveToFirst(), true, "Updated history entry found"); + + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE, + "Updated history entry has correct unchanged title"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), TEST_URL_2, + "Updated history entry has correct unchanged URL"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS)), 11L, + "Updated history entry has correct number of visits"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED)), dateCreated, + "Updated history entry has same creation date"); + mAsserter.isnot(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED)), dateModified, + "Updated history entry has new modification date"); + c.close(); + + } + } + + private class TestInsertHistoryThumbnails extends TestCase { + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + + final String thumbnail = "THUMBNAIL"; + final String pageUrl = h.getAsString(BrowserContract.History.URL); + + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + + // Insert the thumbnail into the thumbnails table + mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, thumbnail)); + + Cursor c = getThumbnailByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"), + thumbnail, "Inserted thumbnail has corresponding thumbnail image"); + c.close(); + } + } + + private class TestUpdateHistoryThumbnails extends TestCase { + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + + final String thumbnail = "THUMBNAIL"; + final String newThumbnail = "NEW_THUMBNAIL"; + final String pageUrl = h.getAsString(BrowserContract.History.URL); + + mProvider.insert(BrowserContract.History.CONTENT_URI, h); + + // Insert the thumbnail into the thumbnails table + mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, thumbnail)); + + Cursor c = getThumbnailByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"), + thumbnail, "Inserted thumbnail has corresponding thumbnail image"); + + ContentValues u = createThumbnailEntry(pageUrl, newThumbnail); + + mProvider.update(BrowserContract.Thumbnails.CONTENT_URI, u, null, null); + c.close(); + + c = getThumbnailByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Updated thumbnail found"); + + mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"), + newThumbnail, "Updated thumbnail has corresponding thumbnail image"); + c.close(); + } + } + + private class TestDeleteHistoryThumbnails extends TestCase { + @Override + public void test() throws Exception { + ContentValues h = createOneHistoryEntry(); + + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + final String pageUrl = h.getAsString(BrowserContract.History.URL); + + // Insert the thumbnail into the thumbnails table + mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, "THUMBNAIL")); + + Cursor c = getThumbnailByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found"); + + mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null); + c.close(); + + c = getThumbnailByUrl(pageUrl); + mAsserter.is(c.moveToFirst(), false, "Thumbnail is deleted with last reference to it"); + c.close(); + } + } + + private class TestInsertUrlAnnotations extends TestCase { + @Override + public void test() throws Exception { + testInsertionViaContentProvider(); + testInsertionViaUrlAnnotations(); + } + + private void testInsertionViaContentProvider() throws Exception { + final String url = "http://mozilla.org"; + final String key = "todo"; + final String value = "v"; + final long dateCreated = System.currentTimeMillis(); + + mProvider.insert(BrowserContract.UrlAnnotations.CONTENT_URI, createUrlAnnotationEntry(url, key, value, dateCreated)); + + final Cursor c = getUrlAnnotationByUrl(url); + try { + mAsserter.is(c.moveToFirst(), true, "Inserted url annotation found"); + assertKeyValueSync(c, key, value); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_CREATED)), dateCreated, + "Inserted url annotation has correct date created"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_MODIFIED)), dateCreated, + "Inserted url annotation has correct date modified"); + } finally { + c.close(); + } + } + + private void testInsertionViaUrlAnnotations() throws Exception { + final String url = "http://hello.org"; + final String key = "toTheUniverse"; + final String value = "42a"; + final long timeBeforeCreation = System.currentTimeMillis(); + + BrowserDB.from(getTestProfile()).getUrlAnnotations().insertAnnotation(mResolver, url, key, value); + + final Cursor c = getUrlAnnotationByUrl(url); + try { + mAsserter.is(c.moveToFirst(), true, "Inserted url annotation found"); + assertKeyValueSync(c, key, value); + mAsserter.is(true, c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_CREATED)) >= timeBeforeCreation, + "Inserted url annotation has date created greater than or equal to time saved before insertion"); + mAsserter.is(true, c.getLong(c.getColumnIndex(BrowserContract.UrlAnnotations.DATE_MODIFIED)) >= timeBeforeCreation, + "Inserted url annotation has correct date modified greater than or equal to time saved before insertion"); + } finally { + c.close(); + } + } + + private void assertKeyValueSync(final Cursor c, final String key, final String value) { + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.UrlAnnotations.KEY)), key, + "Inserted url annotation has correct key"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.UrlAnnotations.VALUE)), value, + "Inserted url annotation has correct value"); + mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.UrlAnnotations.SYNC_STATUS)), SyncStatus.NEW.getDBValue(), + "Inserted url annotation has default sync status"); + } + } + + private class TestInsertUrlMetadata extends TestCase { + @Override + public void test() throws Exception { + testInsertionViaContentProvider(); + testInsertionViaUrlMetadata(); + // testRetrievalViaUrlMetadata depends on data added in the previous two tests + testRetrievalViaUrlMetadata(); + } + + final String url1 = "http://mozilla.org"; + final String url2 = "http://hello.org"; + + private void testInsertionViaContentProvider() throws Exception { + final String tileImage = "http://mozilla.org/tileImage.png"; + final String tileColor = "#FF0000"; + final String touchIcon = "http://mozilla.org/touchIcon.png"; + + // We can only use update since the redirection machinery doesn't exist for insert + mProvider.update(URLMetadataTable.CONTENT_URI.buildUpon().appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(), + createUrlMetadataEntry(url1, tileImage, tileColor, touchIcon), + URLMetadataTable.URL_COLUMN + "=?", + new String[] {url1} + ); + + final Cursor c = getUrlMetadataByUrl(url1); + try { + mAsserter.is(c.getCount(), 1, "URL metadata inserted via Content Provider not found"); + } finally { + c.close(); + } + } + + private void testInsertionViaUrlMetadata() throws Exception { + final String tileImage = "http://hello.org/tileImage.png"; + final String tileColor = "#FF0000"; + final String touchIcon = "http://hello.org/touchIcon.png"; + + final Map<String, Object> data = new HashMap<>(); + data.put(URLMetadataTable.URL_COLUMN, url2); + data.put(URLMetadataTable.TILE_IMAGE_URL_COLUMN, tileImage); + data.put(URLMetadataTable.TILE_COLOR_COLUMN, tileColor); + data.put(URLMetadataTable.TOUCH_ICON_COLUMN, touchIcon); + + BrowserDB.from(getTestProfile()).getURLMetadata().save(mResolver, data); + + final Cursor c = getUrlMetadataByUrl(url2); + try { + mAsserter.is(c.moveToFirst(), true, "URL metadata inserted via UrlMetadata not found"); + } finally { + c.close(); + } + } + + private void testRetrievalViaUrlMetadata() { + // LocalURLMetadata has some caching of results: we need to test that this caching + // doesn't prevent us from accessing data that might not have been loaded into the cache. + // We do this by first doing queries with a subset of data, then later querying additional + // data for a given URL. E.g. even if the first query results in only the requested + // column being cached, the subsequent query should still retrieve all requested columns. + // (In this case the URL may be cached but without all data, we need to make sure that + // this state is correctly handled.) + URLMetadata metadata = BrowserDB.from(getTestProfile()).getURLMetadata(); + + Map<String, Map<String, Object>> results; + Map<String, Object> urlData; + + // 1: retrieve just touch Icons for URL 1 + results = metadata.getForURLs(mResolver, + Collections.singletonList(url1), + Collections.singletonList(URLMetadataTable.TOUCH_ICON_COLUMN)); + + mAsserter.is(results.containsKey(url1), true, "URL 1 not found in results"); + + urlData = results.get(url1); + mAsserter.is(urlData.containsKey(URLMetadataTable.TOUCH_ICON_COLUMN), true, "touchIcon column missing in UrlMetadata results"); + + // 2: retrieve just tile color for URL 2 + results = metadata.getForURLs(mResolver, + Collections.singletonList(url2), + Collections.singletonList(URLMetadataTable.TILE_COLOR_COLUMN)); + + mAsserter.is(results.containsKey(url2), true, "URL 2 not found in results"); + + urlData = results.get(url2); + mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_COLOR_COLUMN), true, "touchIcon column missing in UrlMetadata results"); + + + // 3: retrieve all columns for both URLs + final List<String> urls = Arrays.asList(url1, url2); + + results = metadata.getForURLs(mResolver, + urls, + Arrays.asList(URLMetadataTable.TILE_IMAGE_URL_COLUMN, + URLMetadataTable.TILE_COLOR_COLUMN, + URLMetadataTable.TOUCH_ICON_COLUMN + )); + + mAsserter.is(results.containsKey(url1), true, "URL 1 not found in results"); + mAsserter.is(results.containsKey(url2), true, "URL 2 not found in results"); + + + for (final String url : urls) { + urlData = results.get(url); + mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_IMAGE_URL_COLUMN), true, "touchIcon column missing in UrlMetadata results"); + mAsserter.is(urlData.containsKey(URLMetadataTable.TILE_COLOR_COLUMN), true, "touchIcon column missing in UrlMetadata results"); + mAsserter.is(urlData.containsKey(URLMetadataTable.TOUCH_ICON_COLUMN), true, "touchIcon column missing in UrlMetadata results"); + } + } + } + + private class TestCombinedView extends TestCase { + @Override + public void test() throws Exception { + final String TITLE_1 = "Test Page 1"; + final String TITLE_2 = "Test Page 2"; + final String TITLE_3_HISTORY = "Test Page 3 (History Entry)"; + final String TITLE_3_BOOKMARK = "Test Page 3 (Bookmark Entry)"; + final String TITLE_3_BOOKMARK2 = "Test Page 3 (Bookmark Entry 2)"; + + final String URL_1 = "http://example1.com"; + final String URL_2 = "http://example2.com"; + final String URL_3 = "http://example3.com"; + + final int VISITS = 10; + final long LAST_VISITED = System.currentTimeMillis(); + + // Create a basic history entry + ContentValues basicHistory = createHistoryEntry(TITLE_1, URL_1, VISITS, LAST_VISITED); + long basicHistoryId = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, basicHistory)); + + // Create a basic bookmark entry + ContentValues basicBookmark = createBookmark(TITLE_2, URL_2, mMobileFolderId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + long basicBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, basicBookmark)); + + // Create a history entry and bookmark entry with the same URL to + // represent a visited bookmark + ContentValues combinedHistory = createHistoryEntry(TITLE_3_HISTORY, URL_3, VISITS, LAST_VISITED); + long combinedHistoryId = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory)); + + + ContentValues combinedBookmark = createBookmark(TITLE_3_BOOKMARK, URL_3, mMobileFolderId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + long combinedBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark)); + + ContentValues combinedBookmark2 = createBookmark(TITLE_3_BOOKMARK2, URL_3, mMobileFolderId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + long combinedBookmarkId2 = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark2)); + + // Create a bookmark folder to make sure it _doesn't_ show up in the results + ContentValues folderBookmark = createBookmark("", "", mMobileFolderId, + BrowserContract.Bookmarks.TYPE_FOLDER, 0, "tags", "description", "keyword"); + mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, folderBookmark); + + // Sort entries by url so we can check them individually + final Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, BrowserContract.Combined.URL); + + try { + mAsserter.is(c.getCount(), 3, "3 combined entries found"); + + // First combined entry is basic history entry + mAsserter.is(c.moveToFirst(), true, "Found basic history entry"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L, + "Combined _id column should always be 0"); + // TODO: Should we change BrowserProvider to make this return -1, not 0? + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), 0L, + "Bookmark id should be 0 for basic history entry"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), basicHistoryId, + "Basic history entry has correct history id"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)), TITLE_1, + "Basic history entry has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_1, + "Basic history entry has correct url"); + mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), VISITS, + "Basic history entry has correct number of visits"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), LAST_VISITED, + "Basic history entry has correct last visit time"); + + // Second combined entry is basic bookmark entry + mAsserter.is(c.moveToNext(), true, "Found basic bookmark entry"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L, + "Combined _id column should always be 0"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), basicBookmarkId, + "Basic bookmark entry has correct bookmark id"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), -1L, + "History id should be -1 for basic bookmark entry"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)), TITLE_2, + "Basic bookmark entry has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_2, + "Basic bookmark entry has correct url"); + mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), -1, + "Visits should be -1 for basic bookmark entry"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), -1L, + "Basic entry has correct last visit time"); + + // Third combined entry is a combined history/bookmark entry + mAsserter.is(c.moveToNext(), true, "Found third combined entry"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined._ID)), 0L, + "Combined _id column should always be 0"); + // The bookmark data (bookmark_id and title) associated with the combined entry is non-deterministic, + // it might end up with data coming from any of the matching bookmark entries. + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)) == combinedBookmarkId || + c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)) == combinedBookmarkId2, true, + "Combined entry has correct bookmark id"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)).equals(TITLE_3_BOOKMARK) || + c.getString(c.getColumnIndex(BrowserContract.Combined.TITLE)).equals(TITLE_3_BOOKMARK2), true, + "Combined entry has title corresponding to bookmark entry"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID)), combinedHistoryId, + "Combined entry has correct history id"); + mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_3, + "Combined entry has correct url"); + mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), VISITS, + "Combined entry has correct number of visits"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED)), LAST_VISITED, + "Combined entry has correct last visit time"); + } finally { + c.close(); + } + } + } + + private class TestCombinedViewDisplay extends TestCase { + @Override + public void test() throws Exception { + final String TITLE_1 = "Test Page 1"; + final String TITLE_2 = "Test Page 2"; + final String TITLE_3_HISTORY = "Test Page 3 (History Entry)"; + final String TITLE_3_BOOKMARK = "Test Page 3 (Bookmark Entry)"; + + final String URL_1 = "http://example.com"; + final String URL_2 = "http://example.org"; + final String URL_3 = "http://examples2.com"; + + final int VISITS = 10; + final long LAST_VISITED = System.currentTimeMillis(); + + // Create a basic history entry + ContentValues basicHistory = createHistoryEntry(TITLE_1, URL_1, VISITS, LAST_VISITED); + ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, basicHistory)); + + // Create a basic bookmark entry + ContentValues basicBookmark = createBookmark(TITLE_2, URL_2, mMobileFolderId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, basicBookmark); + + // Create a history entry and bookmark entry with the same URL to + // represent a visited bookmark + ContentValues combinedHistory = createHistoryEntry(TITLE_3_HISTORY, URL_3, VISITS, LAST_VISITED); + mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory); + + ContentValues combinedBookmark = createBookmark(TITLE_3_BOOKMARK, URL_3, mMobileFolderId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark); + + final Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null); + try { + mAsserter.is(c.getCount(), 3, "3 combined entries found"); + } finally { + c.close(); + } + } + } + + private class TestCombinedViewWithDeletedBookmark extends TestCase { + @Override + public void test() throws Exception { + final String TITLE = "Test Page 1"; + final String URL = "http://example.com"; + final int VISITS = 10; + final long LAST_VISITED = System.currentTimeMillis(); + + // Create a combined history entry + ContentValues combinedHistory = createHistoryEntry(TITLE, URL, VISITS, LAST_VISITED); + mProvider.insert(BrowserContract.History.CONTENT_URI, combinedHistory); + + // Create a combined bookmark entry + ContentValues combinedBookmark = createBookmark(TITLE, URL, mMobileFolderId, + BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword"); + long combinedBookmarkId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, combinedBookmark)); + + Cursor c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null); + mAsserter.is(c.getCount(), 1, "1 combined entry found"); + + mAsserter.is(c.moveToFirst(), true, "Found combined entry with bookmark id"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), combinedBookmarkId, + "Bookmark id should be set correctly on combined entry"); + + int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI, + BrowserContract.Bookmarks._ID + " = ?", + new String[] { String.valueOf(combinedBookmarkId) }); + + mAsserter.is((deleted == 1), true, "Inserted combined bookmark was deleted"); + c.close(); + + c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null); + mAsserter.is(c.getCount(), 1, "1 combined entry found"); + + mAsserter.is(c.moveToFirst(), true, "Found combined entry without bookmark id"); + mAsserter.is(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID)), 0L, + "Bookmark id should not be set to removed bookmark id"); + c.close(); + } + } + + /* + * Verify that insert, update, delete, and bulkInsert operations + * notify the ambient content resolver. Each operation calls the + * content resolver notifyChange method synchronously, so it is + * okay to test sequentially. + */ + private class TestBrowserProviderNotifications extends TestCase { + public static final String LOGTAG = "TestBPNotifications"; + + protected void ensureOnlyChangeNotifiedStartsWith(Uri expectedUri, String operation) { + if (expectedUri == null) { + throw new IllegalArgumentException("expectedUri must not be null"); + } + + if (mResolver.notifyChangeList.size() != 1) { + // Log to help post-mortem debugging + Log.w(LOGTAG, "after operation, notifyChangeList = " + mResolver.notifyChangeList); + } + + mAsserter.is((long) mResolver.notifyChangeList.size(), + 1L, + "Content observer was notified exactly once by " + operation); + + Uri uri = mResolver.notifyChangeList.poll(); + + mAsserter.isnot(uri, + null, + "Notification from " + operation + " was valid"); + + mAsserter.ok(uri.toString().startsWith(expectedUri.toString()), + "Content observer was notified exactly once by " + operation, + uri.toString() + " starts with expected prefix " + expectedUri); + } + + @Override + public void test() throws Exception { + // Insert + final ContentValues h = createOneHistoryEntry(); + + mResolver.notifyChangeList.clear(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h)); + + mAsserter.isnot(id, + -1L, + "Inserted item has valid id"); + + ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "insert"); + + // Update + mResolver.notifyChangeList.clear(); + h.put(BrowserContract.History.TITLE, "http://newexample.com"); + + long numUpdated = mProvider.update(BrowserContract.History.CONTENT_URI, h, + BrowserContract.History._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is(numUpdated, + 1L, + "Correct number of items are updated"); + + ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "update"); + + // Delete + mResolver.notifyChangeList.clear(); + long numDeleted = mProvider.delete(BrowserContract.History.CONTENT_URI, null, null); + + mAsserter.is(numDeleted, + 1L, + "Correct number of items are deleted"); + + ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "delete"); + + // Bulk insert + final ContentValues[] hs = new ContentValues[] { createOneHistoryEntry() }; + + mResolver.notifyChangeList.clear(); + long numBulkInserted = mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, hs); + + mAsserter.is(numBulkInserted, + 1L, + "Correct number of items are bulkInserted"); + + ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "bulkInsert"); + } + } + + /** + * Assert that the provided cursor has the expected number of rows, + * closing the cursor afterwards. + */ + private void assertCountIsAndClose(Cursor c, int expectedCount, String message) { + try { + mAsserter.is(c.getCount(), expectedCount, message); + } finally { + c.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java new file mode 100644 index 000000000..d8fc793fc --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBrowserSearchVisibility.java @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.support.v4.app.Fragment; +import android.view.KeyEvent; +import android.view.View; + +import com.robotium.solo.Condition; + +/** + * Test for browser search visibility. + * Sends queries from url bar input and verifies that browser search + * visibility is correct. + */ +public class testBrowserSearchVisibility extends BaseTest { + public void testSearchSuggestions() { + blockForGeckoReady(); + + focusUrlBar(); + + // search should not be visible when editing mode starts + assertBrowserSearchVisibility(false); + + mActions.sendKeys("a"); + + // search should be visible when entry is not empty + assertBrowserSearchVisibility(true); + + mActions.sendKeys("b"); + + // search continues to be visible when more text is added + assertBrowserSearchVisibility(true); + + mActions.sendKeyCode(KeyEvent.KEYCODE_DEL); + + // search continues to be visible when not all text is deleted + assertBrowserSearchVisibility(true); + + mActions.sendKeyCode(KeyEvent.KEYCODE_DEL); + + // search should not be visible, entry is empty now + assertBrowserSearchVisibility(false); + } + + private void assertBrowserSearchVisibility(final boolean isVisible) { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + final Fragment browserSearch = getBrowserSearch(); + + // The fragment should not be present at all. Testing if the + // fragment is present but has no defined view is not a valid + // state. + if (browserSearch == null) + return !isVisible; + + final View v = browserSearch.getView(); + if (isVisible && v != null && v.getVisibility() == View.VISIBLE) + return true; + + return false; + } + }, 5000); + } +} + diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java new file mode 100644 index 000000000..fe3c047a3 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBug1217581.java @@ -0,0 +1,31 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + + +import org.mozilla.gecko.Telemetry; + +public class testBug1217581 extends BaseTest { + // Take arbitrary histogram names used by Fennec. + private static final String TEST_HISTOGRAM_NAME = "FENNEC_SYNC_NUMBER_OF_SYNCS_COMPLETED"; + private static final String TEST_KEYED_HISTOGRAM_NAME = "FX_MIGRATION_ERRORS"; + private static final String TEST_KEY_NAME = "testBug1217581"; + + + public void testBug1217581() { + blockForGeckoReady(); + + mAsserter.ok(true, "Checking that adding to a keyed histogram then adding to a normal histogram does not cause a crash.", ""); + Telemetry.addToKeyedHistogram(TEST_KEYED_HISTOGRAM_NAME, TEST_KEY_NAME, 1); + Telemetry.addToHistogram(TEST_HISTOGRAM_NAME, 1); + mAsserter.ok(true, "Adding to a keyed histogram then to a normal histogram was a success!", ""); + + mAsserter.ok(true, "Checking that adding to a normal histogram then adding to a keyed histogram does not cause a crash.", ""); + Telemetry.addToHistogram(TEST_HISTOGRAM_NAME, 1); + Telemetry.addToKeyedHistogram(TEST_KEYED_HISTOGRAM_NAME, TEST_KEY_NAME, 1); + mAsserter.ok(true, "Adding to a normal histogram then to a keyed histogram was a success!", ""); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java new file mode 100644 index 000000000..fc538b5bf --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck2.java @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.json.JSONObject; + +public class testCheck2 extends PixelTest { + @Override + protected Type getTestType() { + return Type.TALOS; + } + + public void testCheck2() { + String url = getAbsoluteUrl("/startup_test/fennecmark/cnn/cnn.com/index.html"); + + // Enable double-tap zooming + setPreferenceAndWaitForChange("browser.ui.zoom.force-user-scalable", true); + + blockForGeckoReady(); + loadAndPaint(url); + + mDriver.setupScrollHandling(); + + /* + * for this test, we load the timecube page, and replay a recorded sequence of events + * that is a user panning/zooming around the page. specific things in the sequence + * include: + * - scroll on one axis followed by scroll on another axis + * - pinch zoom (in and out) + * - double-tap zoom (in and out) + * - multi-fling panning with different velocities on each fling + * + * this checkerboarding metric is going to be more of a "functional" style test than + * a "unit" style test; i.e. it covers a little bit of a lot of things to measure + * overall performance, but doesn't really allow identifying which part is slow. + */ + + MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(), + mDriver.getGeckoWidth(), mDriver.getGeckoHeight()); + + float completeness = 0.0f; + mDriver.startCheckerboardRecording(); + // replay the events + try { + mer.replayEvents(getAsset("testcheck2-motionevents")); + // give it some time to draw any final frames + Thread.sleep(1000); + completeness = mDriver.stopCheckerboardRecording(); + } catch (Exception e) { + e.printStackTrace(); + mAsserter.ok(false, "Exception while replaying events", e.toString()); + } + + mAsserter.dumpLog("__start_report" + completeness + "__end_report"); + System.out.println("Completeness score: " + completeness); + long msecs = System.currentTimeMillis(); + mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java new file mode 100644 index 000000000..28915bdbc --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testCheck3.java @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.json.JSONObject; + +public class testCheck3 extends PixelTest { + @Override + protected Type getTestType() { + return Type.TALOS; + } + + public void testCheck3() { + String url = getAbsoluteUrl("/facebook.com/www.facebook.com/barackobama.html"); + + // Enable double-tap zooming + setPreferenceAndWaitForChange("browser.ui.zoom.force-user-scalable", true); + + blockForGeckoReady(); + loadAndPaint(url); + + mDriver.setupScrollHandling(); + + /* + * for this test, we load the timecube page, and replay a recorded sequence of events + * that is a user panning/zooming around the page. specific things in the sequence + * include: + * - scroll on one axis followed by scroll on another axis + * - pinch zoom (in and out) + * - double-tap zoom (in and out) + * - multi-fling panning with different velocities on each fling + * + * this checkerboarding metric is going to be more of a "functional" style test than + * a "unit" style test; i.e. it covers a little bit of a lot of things to measure + * overall performance, but doesn't really allow identifying which part is slow. + */ + + MotionEventReplayer mer = new MotionEventReplayer(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop(), + mDriver.getGeckoWidth(), mDriver.getGeckoHeight()); + + float completeness = 0.0f; + mDriver.startCheckerboardRecording(); + // replay the events + try { + mer.replayEvents(getAsset("testcheck2-motionevents")); + // give it some time to draw any final frames + Thread.sleep(1000); + completeness = mDriver.stopCheckerboardRecording(); + } catch (Exception e) { + e.printStackTrace(); + mAsserter.ok(false, "Exception while replaying events", e.toString()); + } + + mAsserter.dumpLog("__start_report" + completeness + "__end_report"); + System.out.println("Completeness score: " + completeness); + long msecs = System.currentTimeMillis(); + mAsserter.dumpLog("__startTimestamp" + msecs + "__endTimestamp"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java new file mode 100644 index 000000000..700c1c255 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDBUtils.java @@ -0,0 +1,70 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import org.mozilla.gecko.db.DBUtils; + +import java.io.File; +import java.io.IOException; + +public class testDBUtils extends BaseTest { + public void testDBUtils() throws IOException { + final File cacheDir = getInstrumentation().getContext().getCacheDir(); + final File dbFile = File.createTempFile("testDBUtils", ".db", cacheDir); + final SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, null); + try { + mAsserter.ok(db != null, "Created DB.", null); + db.execSQL("CREATE TABLE foo (x INTEGER NOT NULL DEFAULT 0, y TEXT)"); + final ContentValues v = new ContentValues(); + v.put("x", 5); + v.put("y", "a"); + db.insert("foo", null, v); + v.put("x", 2); + v.putNull("y"); + db.insert("foo", null, v); + v.put("x", 3); + v.put("y", "z"); + db.insert("foo", null, v); + + DBUtils.UpdateOperation[] ops = {DBUtils.UpdateOperation.BITWISE_OR, DBUtils.UpdateOperation.ASSIGN}; + ContentValues[] values = {new ContentValues(), new ContentValues()}; + values[0].put("x", 0xff); + values[1].put("y", "hello"); + + final int updated = DBUtils.updateArrays(db, "foo", values, ops, "x >= 3", null); + + mAsserter.ok(updated == 2, "Updated two rows.", null); + final Cursor out = db.query("foo", new String[]{"x", "y"}, null, null, null, null, "x"); + try { + mAsserter.ok(out.moveToNext(), "Has first result.", null); + mAsserter.ok(2 == out.getInt(0), "1: First column was untouched.", null); + mAsserter.ok(out.isNull(1), "1: Second column was untouched.", null); + + mAsserter.ok(out.moveToNext(), "Has second result.", null); + mAsserter.ok((0xff | 3) == out.getInt(0), "2: First column was ORed correctly.", null); + mAsserter.ok("hello".equals(out.getString(1)), "2: Second column was assigned correctly.", null); + + mAsserter.ok(out.moveToNext(), "Has third result.", null); + mAsserter.ok((0xff | 5) == out.getInt(0), "3: First column was ORed correctly.", null); + mAsserter.ok("hello".equals(out.getString(1)), "3: Second column was assigned correctly.", null); + + mAsserter.ok(!out.moveToNext(), "No more results.", null); + } finally { + out.close(); + } + + } finally { + try { + db.close(); + } catch (Exception e) { + } + dbFile.delete(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java new file mode 100644 index 000000000..4cc08cc5c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDistribution.java @@ -0,0 +1,556 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.util.Locale; +import java.util.jar.JarInputStream; +import java.util.NoSuchElementException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.BrowserLocaleManager; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.distribution.Distribution; +import org.mozilla.gecko.distribution.ReferrerDescriptor; +import org.mozilla.gecko.distribution.ReferrerReceiver; +import org.mozilla.gecko.preferences.DistroSharedPrefsImport; +import org.mozilla.gecko.preferences.GeckoPreferences; +import org.mozilla.gecko.util.ThreadUtils; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; + +/** + * Tests distribution customization. + * mock-package.zip should contain the following directory structure: + * + * distribution/ + * preferences.json + * bookmarks.json + * searchplugins/ + * common/ + * engine.xml + * suggestedsites/ + * locales/ + * en-US/ + * suggestedsites.json + * extensions/ + * distribution.test@mozilla.org.xpi + */ +public class testDistribution extends ContentProviderTest { + private static final String CLASS_REFERRER_RECEIVER = "org.mozilla.gecko.distribution.ReferrerReceiver"; + private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER"; + private static final int WAIT_TIMEOUT_MSEC = 10000; + public static final String LOGTAG = "GeckoTestDistribution"; + + public static class TestableDistribution extends Distribution { + @Override + protected JarInputStream fetchDistribution(URI uri, + HttpURLConnection connection) throws IOException { + Log.i(LOGTAG, "Not downloading: this is a test."); + return null; + } + + public TestableDistribution(Context context) { + super(context); + } + + public void go() { + doInit(); + } + + public static void clearReferrerDescriptorForTesting() { + referrer = null; + } + + public static ReferrerDescriptor getReferrerDescriptorForTesting() { + return referrer; + } + } + + private static final String MOCK_PACKAGE = "mock-package.zip"; + private static final int PREF_REQUEST_ID = 0x7357; + + private Activity mActivity; + + /** + * This is a hack. + * + * Startup results in us writing prefs -- we fetch the Distribution, which + * caches its state. Our tests try to wipe those prefs, but apparently + * sometimes race with startup, which leads to us not getting one of our + * expected messages. The test fails. + * + * This hack waits for any existing background tasks -- such as the one that + * writes prefs -- to finish before we begin the test. + */ + private void waitForBackgroundHappiness() { + final Object signal = new Object(); + final Runnable done = new Runnable() { + @Override + public void run() { + synchronized (signal) { + signal.notify(); + } + } + }; + synchronized (signal) { + ThreadUtils.postToBackgroundThread(done); + try { + signal.wait(); + } catch (InterruptedException e) { + mAsserter.ok(false, "InterruptedException waiting on background thread.", e.toString()); + } + } + mAsserter.dumpLog("Background task completed. Proceeding."); + } + + public void testDistribution() throws Exception { + mActivity = getActivity(); + + String mockPackagePath = getMockPackagePath(); + + // Wait for any startup-related background distribution shenanigans to + // finish. This reduces the chance of us racing with startup pref writes. + waitForBackgroundHappiness(); + + // Pre-clear distribution pref, run basic preferences and en-US localized preferences Tests + clearDistributionPref(); + clearDistributionFromDataData(); + + setTestLocale("en-US"); + try { + initDistribution(mockPackagePath); + } catch(NoSuchElementException e) { + // TODO: determine why this exception is intermittently thrown + Log.w(LOGTAG, "NoSuchElementException on first initDistribution -- will retry"); + mSolo.sleep(4000); + initDistribution(mockPackagePath); + } + checkPreferences(); + checkAndroidPreferences(); + checkLocalizedPreferences("en-US"); + checkSearchPlugin(); + checkAddon(); + + // Pre-clear distribution pref, and run es-MX localized preferences Test + clearDistributionPref(); + clearDistributionFromDataData(); + setTestLocale("es-MX"); + initDistribution(mockPackagePath); + checkLocalizedPreferences("es-MX"); + + // Test the (stubbed) download interaction. + setTestLocale("en-US"); + clearDistributionPref(); + clearDistributionFromDataData(); + doTestValidReferrerIntent(); + + clearDistributionPref(); + clearDistributionFromDataData(); + doTestInvalidReferrerIntent(); + } + + private void setOSLocale(Locale locale) { + Locale.setDefault(locale); + BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(mActivity), locale); + } + + private abstract class ExpectNoDistributionCallback implements Distribution.ReadyCallback { + @Override + public void distributionFound(final Distribution distribution) { + mAsserter.ok(false, "No distributionFound.", "Wasn't expecting a distribution!"); + synchronized (distribution) { + distribution.notifyAll(); + } + } + + @Override + public void distributionArrivedLate(final Distribution distribution) { + mAsserter.ok(false, "No distributionArrivedLate.", "Wasn't expecting a late distribution!"); + } + } + + private void doReferrerTest(String ref, final TestableDistribution distribution, final Distribution.ReadyCallback distributionReady) throws InterruptedException { + final Intent intent = new Intent(ACTION_INSTALL_REFERRER); + intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER); + intent.putExtra("referrer", ref); + + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(LOGTAG, "Test received " + intent.getAction()); + + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + distribution.addOnDistributionReadyCallback(distributionReady); + distribution.go(); + } + }); + } + }; + + IntentFilter intentFilter = new IntentFilter(ReferrerReceiver.ACTION_REFERRER_RECEIVED); + final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(mActivity); + localBroadcastManager.registerReceiver(receiver, intentFilter); + + Log.i(LOGTAG, "Broadcasting referrer intent."); + try { + mActivity.sendBroadcast(intent, null); + synchronized (distribution) { + distribution.wait(WAIT_TIMEOUT_MSEC); + } + } finally { + localBroadcastManager.unregisterReceiver(receiver); + } + } + + public void doTestValidReferrerIntent() throws Exception { + // Equivalent to + // am broadcast -a com.android.vending.INSTALL_REFERRER \ + // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \ + // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution" + final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution"; + final TestableDistribution distribution = new TestableDistribution(mActivity); + final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() { + @Override + public void distributionNotFound() { + Log.i(LOGTAG, "Test told distribution processing is done."); + mAsserter.ok(!distribution.exists(), "Not processed.", "No download because we're offline."); + ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting(); + mAsserter.dumpLog("Referrer was " + referrerValue); + mAsserter.is(referrerValue.content, "testcontent", "Referrer content"); + mAsserter.is(referrerValue.medium, "testmedium", "Referrer medium"); + mAsserter.is(referrerValue.campaign, "distribution", "Referrer campaign"); + synchronized (distribution) { + distribution.notifyAll(); + } + } + }; + + doReferrerTest(ref, distribution, distributionReady); + } + + /** + * Test processing if the campaign isn't "distribution". The intent shouldn't + * result in a download, and won't be saved as the temporary referrer, + * even if we *do* include it in a Campaign:Set message. + */ + public void doTestInvalidReferrerIntent() throws Exception { + // Equivalent to + // am broadcast -a com.android.vending.INSTALL_REFERRER \ + // -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \ + // --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname" + final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname"; + final TestableDistribution distribution = new TestableDistribution(mActivity); + final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() { + @Override + public void distributionNotFound() { + mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong."); + ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting(); + mAsserter.is(referrerValue, null, "No referrer."); + synchronized (distribution) { + distribution.notifyAll(); + } + } + }; + + doReferrerTest(ref, distribution, distributionReady); + } + + // Initialize the distribution from the mock package. + private Distribution initDistribution(String aPackagePath) { + // Call Distribution.init with the mock package. + Actions.EventExpecter distributionSetExpecter = mActions.expectGeckoEvent("Distribution:Set:OK"); + Distribution dist = Distribution.init(mActivity, aPackagePath, "prefs-" + System.currentTimeMillis()); + distributionSetExpecter.blockForEvent(); + distributionSetExpecter.unregisterListener(); + DistroSharedPrefsImport.importPreferences(mActivity, dist); + return dist; + } + + // Test distribution and preferences values stored in preferences.json + private void checkPreferences() { + String prefID = "distribution.id"; + String prefAbout = "distribution.about"; + String prefVersion = "distribution.version"; + String prefTestBoolean = "distribution.test.boolean"; + String prefTestString = "distribution.test.string"; + String prefTestInt = "distribution.test.int"; + + try { + final String[] prefNames = { prefID, + prefAbout, + prefVersion, + prefTestBoolean, + prefTestString, + prefTestInt }; + + final JSONArray preferences = getPrefs(prefNames); + for (int i = 0; i < preferences.length(); i++) { + JSONObject pref = (JSONObject) preferences.get(i); + String name = pref.getString("name"); + + if (name.equals(prefID)) { + mAsserter.is(pref.getString("value"), "test-partner", "check " + prefID); + } else if (name.equals(prefAbout)) { + mAsserter.is(pref.getString("value"), "Test Partner", "check " + prefAbout); + } else if (name.equals(prefVersion)) { + mAsserter.is(pref.getInt("value"), 1, "check " + prefVersion); + } else if (name.equals(prefTestBoolean)) { + mAsserter.is(pref.getBoolean("value"), true, "check " + prefTestBoolean); + } else if (name.equals(prefTestString)) { + mAsserter.is(pref.getString("value"), "test", "check " + prefTestString); + } else if (name.equals(prefTestInt)) { + mAsserter.is(pref.getInt("value"), 5, "check " + prefTestInt); + } + } + + } catch (JSONException e) { + mAsserter.ok(false, "exception getting preferences", e.toString()); + } + } + + private void checkAndroidPreferences() { + final SharedPreferences sharedPreferences = GeckoSharedPrefs.forProfile(getActivity()); + String prefTestBoolean = "android.distribution.test.boolean"; + String prefTestString = "android.distribution.test.string"; + String prefTestInt = "android.distribution.test.int"; + String prefTestLong = "android.distribution.test.long"; + + final String[] prefNames = { prefTestBoolean, + prefTestString, + prefTestInt, + prefTestLong }; + + try { + for (String name : prefNames) { + if (name.equals(prefTestBoolean)) { + mAsserter.is(sharedPreferences.getBoolean(GeckoPreferences.NON_PREF_PREFIX + name, false), true, "check " + prefTestBoolean); + } else if (name.equals(prefTestString)) { + mAsserter.is(sharedPreferences.getString(GeckoPreferences.NON_PREF_PREFIX + name, ""), "test", "check " + prefTestString); + } else if (name.equals(prefTestInt)) { + mAsserter.is(sharedPreferences.getInt(GeckoPreferences.NON_PREF_PREFIX + name, 0), 1, "check " + prefTestInt); + } else if (name.equals(prefTestLong)) { + mAsserter.is(sharedPreferences.getLong(GeckoPreferences.NON_PREF_PREFIX + name, 0), 2147483648l, "check " + prefTestLong); + } + } + } catch (ClassCastException e) { + mAsserter.ok(false, "exception getting preferences", e.toString()); + } + } + + private void checkSearchPlugin() { + Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("SearchEngines:Data"); + mActions.sendGeckoEvent("SearchEngines:GetVisible", null); + + try { + JSONObject data = new JSONObject(eventExpecter.blockForEventData()); + eventExpecter.unregisterListener(); + JSONArray searchEngines = data.getJSONArray("searchEngines"); + boolean foundEngine = false; + for (int i = 0; i < searchEngines.length(); i++) { + JSONObject engine = (JSONObject) searchEngines.get(i); + String name = engine.getString("name"); + if (name.equals("Test search engine")) { + foundEngine = true; + break; + } + } + mAsserter.ok(foundEngine, "check search plugin", "found test search plugin"); + } catch (JSONException e) { + mAsserter.ok(false, "exception getting search plugins", e.toString()); + } + } + + private void checkAddon() { + try { + final String[] prefNames = { "distribution.test.addonEnabled" }; + final JSONArray preferences = getPrefs(prefNames); + final JSONObject pref = (JSONObject) preferences.get(0); + mAsserter.is(pref.getBoolean("value"), true, "check distribution add-on is enabled"); + } catch (JSONException e) { + mAsserter.ok(false, "exception getting preferences", e.toString()); + } + } + + private JSONArray getPrefs(String[] prefNames) throws JSONException { + final JSONArray result = new JSONArray(); + + mActions.getPrefs(prefNames, new Actions.PrefHandlerBase() { + private void addItem(String pref, Object value) { + try { + final JSONObject item = new JSONObject(); + item.put("name", pref).put("value", value); + result.put(item); + } catch (final JSONException e) { + mAsserter.ok(false, "exception getting prefs", e.toString()); + } + } + + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, boolean value) { + addItem(pref, value); + } + + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, int value) { + addItem(pref, value); + } + + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, String value) { + addItem(pref, value); + } + }).waitForFinish(); + + return result; + } + + // Sets the distribution locale preference for the test. + private void setTestLocale(String locale) { + BrowserLocaleManager.getInstance().setSelectedLocale(mActivity, locale); + } + + // Test localized distribution and preferences values stored in preferences.json + private void checkLocalizedPreferences(final String aLocale) { + final String prefAbout = "distribution.about"; + final String prefLocalizeable = "distribution.test.localizeable"; + final String prefLocalizeableOverride = "distribution.test.localizeable-override"; + final String[] prefNames = { prefAbout, prefLocalizeable, prefLocalizeableOverride }; + + mActions.getPrefs(prefNames, new Actions.PrefHandlerBase() { + @Override // Actions.PrefHandlerBase + public void prefValue(String name, String value) { + if (name.equals(prefAbout)) { + if (aLocale.equals("en-US")) { + mAsserter.is(value, "Test Partner", "check " + prefAbout); + } else if (aLocale.equals("es-MX")) { + mAsserter.is(value, "Afiliado de Prueba", "check " + prefAbout); + } + } else if (name.equals(prefLocalizeable)) { + if (aLocale.equals("en-US")) { + mAsserter.is(value, "http://test.org/en-US/en-US/", "check " + prefLocalizeable); + } else if (aLocale.equals("es-MX")) { + mAsserter.is(value, "http://test.org/es-MX/es-MX/", "check " + prefLocalizeable); + } + } else if (name.equals(prefLocalizeableOverride)) { + if (aLocale.equals("en-US")) { + mAsserter.is(value, "http://cheese.com", "check " + prefLocalizeableOverride); + } else if (aLocale.equals("es-MX")) { + mAsserter.is(value, "http://test.org/es-MX/", "check " + prefLocalizeableOverride); + } + } else { + // Raise exception. + super.prefValue(name, value); + } + } + }).waitForFinish(); + } + + // Copies the mock package to the data directory and returns the file path to it. + private String getMockPackagePath() { + String mockPackagePath = ""; + + try { + InputStream inStream = getAsset(MOCK_PACKAGE); + File dataDir = new File(mActivity.getApplicationInfo().dataDir); + File outFile = new File(dataDir, MOCK_PACKAGE); + + OutputStream outStream = new FileOutputStream(outFile); + int b; + while ((b = inStream.read()) != -1) { + outStream.write(b); + } + inStream.close(); + outStream.close(); + + mockPackagePath = outFile.getPath(); + + } catch (Exception e) { + mAsserter.ok(false, "exception copying mock distribution package to data directory", e.toString()); + } + + return mockPackagePath; + } + + /** + * Clears the distribution pref to return distribution state to STATE_UNKNOWN, + * and wipes the in-memory referrer pigeonhole. + */ + private void clearDistributionPref() { + mAsserter.dumpLog("Clearing distribution pref."); + SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE); + String keyName = mActivity.getPackageName() + ".distribution_state"; + settings.edit().remove(keyName).commit(); + TestableDistribution.clearReferrerDescriptorForTesting(); + } + + /** + * Clears any distribution found in /data/data. + */ + private void clearDistributionFromDataData() throws Exception { + File dataDir = new File(mActivity.getApplicationInfo().dataDir); + + // Recursively delete distribution files that Distribution.init copied to data directory. + File distDir = new File(dataDir, "distribution"); + if (distDir.exists()) { + mAsserter.dumpLog("Clearing distribution from " + distDir.getAbsolutePath()); + delete(distDir); + } else { + mAsserter.dumpLog("No distribution to clear from " + distDir.getAbsolutePath()); + } + } + + @Override + public void setUp() throws Exception { + // TODO: Set up the content provider after setting the distribution. + super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db"); + } + + private void delete(File file) throws Exception { + if (file.isDirectory()) { + File[] files = file.listFiles(); + for (File f : files) { + delete(f); + } + } + mAsserter.ok(file.delete(), "clean up distribution files", "deleted " + file.getPath()); + } + + @Override + public void tearDown() throws Exception { + File dataDir = new File(mActivity.getApplicationInfo().dataDir); + + // Delete mock package from data directory. + File mockPackage = new File(dataDir, MOCK_PACKAGE); + mAsserter.ok(mockPackage.delete(), "clean up mock package", "deleted " + mockPackage.getPath()); + + clearDistributionFromDataData(); + clearDistributionPref(); + + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java new file mode 100644 index 000000000..2c3feb3a8 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.widget.CheckBox; +import android.view.View; +import com.robotium.solo.Condition; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.Actions; + +/* This test will test if doorhangers are displayed and dismissed + The test will test: + * geolocation doorhangers - sharing and not sharing the location dismisses the doorhanger + * opening a new tab hides the doorhanger + * offline storage permission doorhangers - allowing and not allowing offline storage dismisses the doorhanger + * Password Manager doorhangers - Remember and Not Now options dismiss the doorhanger +*/ +public class testDoorHanger extends BaseTest { + private boolean offlineAllowedByDefault = true; + + public void testDoorHanger() { + String GEO_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_GEOLOCATION_URL); + String BLANK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + String OFFLINE_STORAGE_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_OFFLINE_STORAGE_URL); + + blockForGeckoReady(); + + // Test geolocation notification + loadUrlAndWait(GEO_URL); + waitForText(mStringHelper.GEO_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), true, "Geolocation doorhanger has been displayed"); + + // Test "Share" button hides the notification + waitForCheckBox(); + mSolo.clickOnCheckBox(0); + mSolo.clickOnButton(mStringHelper.GEO_ALLOW); + waitForTextDismissed(mStringHelper.GEO_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), false, "Geolocation doorhanger has been hidden when allowing share"); + + // Re-trigger geolocation notification + loadUrlAndWait(GEO_URL); + waitForText(mStringHelper.GEO_MESSAGE); + + // Test "Don't share" button hides the notification + waitForCheckBox(); + mSolo.clickOnCheckBox(0); + mSolo.clickOnButton(mStringHelper.GEO_DENY); + waitForTextDismissed(mStringHelper.GEO_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.GEO_MESSAGE), false, "Geolocation doorhanger has been hidden when denying share"); + + /* FIXME: disabled on fig - bug 880060 (for some reason this fails because of some raciness) + // Re-trigger geolocation notification + loadUrlAndWait(GEO_URL); + waitForText(GEO_MESSAGE); + + // Add a new tab + addTab(BLANK_URL); + + // Make sure doorhanger is hidden + mAsserter.is(mSolo.searchText(GEO_MESSAGE), false, "Geolocation doorhanger notification is hidden when opening a new tab"); + */ + + // Save offline-allow-by-default preferences first + mActions.getPrefs(new String[] { "offline-apps.allow_by_default" }, + new Actions.PrefHandlerBase() { + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, boolean value) { + mAsserter.is(pref, "offline-apps.allow_by_default", "Expecting correct pref name"); + offlineAllowedByDefault = value; + } + }).waitForFinish(); + + setPreferenceAndWaitForChange("offline-apps.allow_by_default", false); + + // Load offline storage page + loadUrlAndWait(OFFLINE_STORAGE_URL); + waitForText(mStringHelper.OFFLINE_MESSAGE); + + // Test doorhanger dismissed when tapping "Don't share" + waitForCheckBox(); + mSolo.clickOnCheckBox(0); + mSolo.clickOnButton(mStringHelper.OFFLINE_DENY); + waitForTextDismissed(mStringHelper.OFFLINE_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger notification is hidden when denying storage"); + + // Load offline storage page + loadUrlAndWait(OFFLINE_STORAGE_URL); + waitForText(mStringHelper.OFFLINE_MESSAGE); + + // Test doorhanger dismissed when tapping "Allow" and is not displayed again + mSolo.clickOnButton(mStringHelper.OFFLINE_ALLOW); + waitForTextDismissed(mStringHelper.OFFLINE_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger notification is hidden when allowing storage"); + loadUrlAndWait(OFFLINE_STORAGE_URL); + mAsserter.is(mSolo.searchText(mStringHelper.OFFLINE_MESSAGE), false, "Offline storage doorhanger is no longer triggered"); + + // Revert offline setting + setPreferenceAndWaitForChange("offline-apps.allow_by_default", offlineAllowedByDefault); + + // Load new login page + loadUrlAndWait(getAbsoluteUrl(mStringHelper.ROBOCOP_LOGIN_01_URL)); + waitForText(mStringHelper.LOGIN_MESSAGE); + + // Test doorhanger is dismissed when tapping "Remember". + mSolo.clickOnButton(mStringHelper.LOGIN_ALLOW); + waitForTextDismissed(mStringHelper.LOGIN_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when allowing saving password"); + + // Load login page + loadUrlAndWait(getAbsoluteUrl(mStringHelper.ROBOCOP_LOGIN_02_URL)); + waitForText(mStringHelper.LOGIN_MESSAGE); + + // Test doorhanger is dismissed when tapping "Never". + mSolo.clickOnButton(mStringHelper.LOGIN_DENY); + waitForTextDismissed(mStringHelper.LOGIN_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.LOGIN_MESSAGE), false, "Login doorhanger notification is hidden when denying saving password"); + + testPopupBlocking(); + } + + private void testPopupBlocking() { + String POPUP_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_POPUP_URL); + + setPreferenceAndWaitForChange("dom.disable_open_during_load", true); + + // Load page with popup + loadUrlAndWait(POPUP_URL); + waitForText(mStringHelper.POPUP_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed"); + + // Wait for the popup to be shown. + Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added"); + + waitForCheckBox(); + mSolo.clickOnCheckBox(0); + mSolo.clickOnButton(mStringHelper.POPUP_ALLOW); + waitForTextDismissed(mStringHelper.POPUP_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), false, "Popup blocker is hidden when popup allowed"); + + try { + final JSONObject data = new JSONObject(tabEventExpecter.blockForEventData()); + + // Check to make sure the popup window was opened. + mAsserter.is("data:text/plain;charset=utf-8,a", data.getString("uri"), "Checking popup URL"); + + // Close the popup window. + closeTab(data.getInt("tabID")); + + } catch (JSONException e) { + mAsserter.ok(false, "exception getting event data", e.toString()); + } + tabEventExpecter.unregisterListener(); + + // Load page with popup + loadUrlAndWait(POPUP_URL); + waitForText(mStringHelper.POPUP_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed"); + + waitForCheckBox(); + mSolo.clickOnCheckBox(0); + mSolo.clickOnButton(mStringHelper.POPUP_DENY); + waitForTextDismissed(mStringHelper.POPUP_MESSAGE); + mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), false, "Popup blocker is hidden when popup denied"); + + // Check that we're on the same page to verify that the popup was not shown. + verifyUrl(POPUP_URL); + + setPreferenceAndWaitForChange("dom.disable_open_during_load", false); + } + + // wait for a CheckBox view that is clickable + private void waitForCheckBox() { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + for (CheckBox view : mSolo.getCurrentViews(CheckBox.class)) { + // checking isClickable alone is not sufficient -- + // intermittent "cannot click" errors persist unless + // additional checks are used + if (view.isClickable() && + view.getVisibility() == View.VISIBLE && + view.getWidth() > 0 && + view.getHeight() > 0) { + return true; + } + } + return false; + } + }, MAX_WAIT_MS); + } + + // wait until the specified text is *not* displayed + private void waitForTextDismissed(final String text) { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return !mSolo.searchText(text); + } + }, MAX_WAIT_MS); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java new file mode 100644 index 000000000..ad40459d5 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testEventDispatcher.java @@ -0,0 +1,450 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.util.BundleEventListener; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.NativeEventListener; +import org.mozilla.gecko.util.NativeJSObject; +import org.mozilla.gecko.util.ThreadUtils; + +import android.os.Bundle; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Tests the proper operation of EventDispatcher, + * including associated NativeJSObject objects. + */ +public class testEventDispatcher extends JavascriptBridgeTest + implements BundleEventListener, GeckoEventListener, NativeEventListener { + + private static final String TEST_JS = "testEventDispatcher.js"; + private static final String GECKO_EVENT = "Robocop:TestGeckoEvent"; + private static final String GECKO_RESPONSE_EVENT = "Robocop:TestGeckoResponse"; + private static final String NATIVE_EVENT = "Robocop:TestNativeEvent"; + private static final String NATIVE_RESPONSE_EVENT = "Robocop:TestNativeResponse"; + private static final String NATIVE_EXCEPTION_EVENT = "Robocop:TestNativeException"; + private static final String UI_EVENT = "Robocop:TestUIEvent"; + private static final String UI_RESPONSE_EVENT = "Robocop:TestUIResponse"; + private static final String BACKGROUND_EVENT = "Robocop:TestBackgroundEvent"; + private static final String BACKGROUND_RESPONSE_EVENT = "Robocop:TestBackgrondResponse"; + + private static final long WAIT_FOR_BUNDLE_EVENT_TIMEOUT_MILLIS = 20000; // 20 seconds + + private NativeJSObject savedMessage; + + private boolean handledGeckoEvent; + private boolean handledNativeEvent; + private boolean handledAsyncEvent; + + @Override + public void setUp() throws Exception { + super.setUp(); + + EventDispatcher.getInstance().registerGeckoThreadListener( + (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT); + EventDispatcher.getInstance().registerGeckoThreadListener( + (NativeEventListener) this, + NATIVE_EVENT, NATIVE_RESPONSE_EVENT, NATIVE_EXCEPTION_EVENT); + EventDispatcher.getInstance().registerUiThreadListener( + this, UI_EVENT, UI_RESPONSE_EVENT); + EventDispatcher.getInstance().registerBackgroundThreadListener( + this, BACKGROUND_EVENT, BACKGROUND_RESPONSE_EVENT); + } + + @Override + public void tearDown() throws Exception { + EventDispatcher.getInstance().unregisterGeckoThreadListener( + (GeckoEventListener) this, GECKO_EVENT, GECKO_RESPONSE_EVENT); + EventDispatcher.getInstance().unregisterGeckoThreadListener( + (NativeEventListener) this, + NATIVE_EVENT, NATIVE_RESPONSE_EVENT, NATIVE_EXCEPTION_EVENT); + EventDispatcher.getInstance().unregisterUiThreadListener( + this, UI_EVENT, UI_RESPONSE_EVENT); + EventDispatcher.getInstance().unregisterBackgroundThreadListener( + this, BACKGROUND_EVENT, BACKGROUND_RESPONSE_EVENT); + + super.tearDown(); + } + + private synchronized void waitForAsyncEvent() { + final long startTime = System.nanoTime(); + while (!handledAsyncEvent) { + if (System.nanoTime() - startTime + >= WAIT_FOR_BUNDLE_EVENT_TIMEOUT_MILLIS * 1e6 /* ns per ms */) { + fFail("Should have completed event before timeout"); + } + try { + wait(1000); // Wait for 1 second at a time. + } catch (final InterruptedException e) { + // Attempt waiting again. + } + } + handledAsyncEvent = false; + } + + private synchronized void notifyAsyncEvent() { + handledAsyncEvent = true; + notifyAll(); + } + + public void testEventDispatcher() { + blockForReadyAndLoadJS(TEST_JS); + + getJS().syncCall("send_test_message", GECKO_EVENT); + fAssertTrue("Should have handled Gecko event synchronously", handledGeckoEvent); + + getJS().syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "success"); + getJS().syncCall("send_message_for_response", GECKO_RESPONSE_EVENT, "error"); + + getJS().syncCall("send_test_message", NATIVE_EVENT); + fAssertTrue("Should have handled native event synchronously", handledNativeEvent); + + getJS().syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "success"); + getJS().syncCall("send_message_for_response", NATIVE_RESPONSE_EVENT, "error"); + + getJS().syncCall("send_test_message", NATIVE_EXCEPTION_EVENT); + + getJS().syncCall("send_test_message", UI_EVENT); + waitForAsyncEvent(); + + getJS().syncCall("send_message_for_response", UI_RESPONSE_EVENT, "success"); + waitForAsyncEvent(); + + getJS().syncCall("send_message_for_response", UI_RESPONSE_EVENT, "error"); + waitForAsyncEvent(); + + getJS().syncCall("send_test_message", BACKGROUND_EVENT); + waitForAsyncEvent(); + + getJS().syncCall("send_message_for_response", BACKGROUND_RESPONSE_EVENT, "success"); + waitForAsyncEvent(); + + getJS().syncCall("send_message_for_response", BACKGROUND_RESPONSE_EVENT, "error"); + waitForAsyncEvent(); + + getJS().syncCall("finish_test"); + } + + @Override + public void handleMessage(final String event, final Bundle message, + final EventCallback callback) { + + if (UI_EVENT.equals(event) || UI_RESPONSE_EVENT.equals(event)) { + fAssertTrue("UI event should be on UI thread", ThreadUtils.isOnUiThread()); + + } else if (BACKGROUND_EVENT.equals(event) || BACKGROUND_RESPONSE_EVENT.equals(event)) { + fAssertTrue("Background event should be on background thread", + ThreadUtils.isOnBackgroundThread()); + + } else { + fFail("Event type should be valid: " + event); + } + + if (UI_EVENT.equals(event) || BACKGROUND_EVENT.equals(event)) { + checkBundle(message); + checkBundle(message.getBundle("object")); + + } else if (UI_RESPONSE_EVENT.equals(event) || BACKGROUND_RESPONSE_EVENT.equals(event)) { + final String response = message.getString("response"); + if ("success".equals(response)) { + callback.sendSuccess(response); + } else if ("error".equals(response)) { + callback.sendError(response); + } else { + fFail("Response type should be valid: " + response); + } + + } else { + fFail("Event type should be valid: " + event); + } + + notifyAsyncEvent(); + } + + @Override + public void handleMessage(final String event, final JSONObject message) { + ThreadUtils.assertOnGeckoThread(); + + try { + if (GECKO_EVENT.equals(event)) { + checkJSONObject(message); + checkJSONObject(message.getJSONObject("object")); + handledGeckoEvent = true; + + } else if (GECKO_RESPONSE_EVENT.equals(event)) { + final String response = message.getString("response"); + if ("success".equals(response)) { + EventDispatcher.sendResponse(message, response); + } else if ("error".equals(response)) { + EventDispatcher.sendError(message, response); + } else { + fFail("Response type should be valid: " + response); + } + + } else { + fFail("Event type should be valid: " + event); + } + } catch (final JSONException e) { + fFail(e.toString()); + } + } + + @Override + public void handleMessage(final String event, final NativeJSObject message, + final EventCallback callback) { + ThreadUtils.assertOnGeckoThread(); + + if (NATIVE_EVENT.equals(event)) { + checkNativeJSObject(message); + checkNativeJSObject(message.getObject("object")); + fAssertNotSame("optObject returns existent value", + null, message.optObject("object", null)); + fAssertSame("optObject returns fallback value if nonexistent", + null, message.optObject("nonexistent_object", null)); + + final NativeJSObject[] objectArray = message.getObjectArray("objectArray"); + fAssertNotNull("Native object array should exist", objectArray); + fAssertEquals("Native object array has correct length", 2, objectArray.length); + fAssertSame("Native object array index 0 has correct value", null, objectArray[0]); + fAssertNotSame("Native object array index 1 has correct value", null, objectArray[1]); + checkNativeJSObject(objectArray[1]); + fAssertNotSame("optObjectArray returns existent value", + null, message.optObjectArray("objectArray", null)); + fAssertSame("optObjectArray returns fallback value if nonexistent", + null, message.optObjectArray("nonexistent_objectArray", null)); + + final Bundle bundle = message.toBundle(); + checkBundle(bundle); + checkBundle(bundle.getBundle("object")); + fAssertNotSame("optBundle returns property value if it exists", + null, message.optBundle("object", null)); + fAssertSame("optBundle returns fallback value if property does not exist", + null, message.optBundle("nonexistent_object", null)); + + final Bundle[] bundleArray = message.getBundleArray("objectArray"); + fAssertNotNull("Native bundle array should exist", bundleArray); + fAssertEquals("Native bundle array has correct length", 2, bundleArray.length); + fAssertSame("Native bundle array index 0 has correct value", null, bundleArray[0]); + fAssertNotSame("Native bundle array index 1 has correct value", null, bundleArray[1]); + checkBundle(bundleArray[1]); + fAssertNotSame("optBundleArray returns existent value", + null, message.optBundleArray("objectArray", null)); + fAssertSame("optBundleArray returns fallback value if nonexistent", + null, message.optBundleArray("nonexistent_objectArray", null)); + + handledNativeEvent = true; + + } else if (NATIVE_RESPONSE_EVENT.equals(event)) { + final String response = message.getString("response"); + if ("success".equals(response)) { + callback.sendSuccess(response); + } else if ("error".equals(response)) { + callback.sendError(response); + } else { + fFail("Response type should be valid: " + response); + } + + // Save this message for post-disposal check. + savedMessage = message; + + } else if (NATIVE_EXCEPTION_EVENT.equals(event)) { + // Make sure we throw the right exceptions. + try { + message.getString(null); + fFail("null property name should throw IllegalArgumentException"); + } catch (final IllegalArgumentException e) { + } + + try { + message.getString("nonexistent_string"); + fFail("Nonexistent property name should throw InvalidPropertyException"); + } catch (final NativeJSObject.InvalidPropertyException e) { + } + + try { + message.getString("int"); + fFail("Wrong property type should throw InvalidPropertyException"); + } catch (final NativeJSObject.InvalidPropertyException e) { + } + + fAssertNotSame("Should have saved a message", null, savedMessage); + try { + savedMessage.toString(); + fFail("Using NativeJSContainer should throw after disposal"); + } catch (final NullPointerException e) { + } + + // Save this test for last; make sure EventDispatcher catches InvalidPropertyException. + message.getString("nonexistent_string"); + fFail("EventDispatcher should catch InvalidPropertyException"); + + } else { + fFail("Event type should be valid: " + event); + } + } + + private void checkBundle(final Bundle bundle) { + fAssertEquals("Bundle boolean has correct value", true, bundle.getBoolean("boolean")); + fAssertEquals("Bundle int has correct value", 1, bundle.getInt("int")); + fAssertEquals("Bundle double has correct value", 0.5, bundle.getDouble("double")); + fAssertEquals("Bundle string has correct value", "foo", bundle.getString("string")); + + final boolean[] booleanArray = bundle.getBooleanArray("booleanArray"); + fAssertNotNull("Bundle boolean array should exist", booleanArray); + fAssertEquals("Bundle boolean array has correct length", 2, booleanArray.length); + fAssertEquals("Bundle boolean array index 0 has correct value", false, booleanArray[0]); + fAssertEquals("Bundle boolean array index 1 has correct value", true, booleanArray[1]); + + final int[] intArray = bundle.getIntArray("intArray"); + fAssertNotNull("Bundle int array should exist", intArray); + fAssertEquals("Bundle int array has correct length", 2, intArray.length); + fAssertEquals("Bundle int array index 0 has correct value", 2, intArray[0]); + fAssertEquals("Bundle int array index 1 has correct value", 3, intArray[1]); + + final double[] doubleArray = bundle.getDoubleArray("doubleArray"); + fAssertNotNull("Bundle double array should exist", doubleArray); + fAssertEquals("Bundle double array has correct length", 2, doubleArray.length); + fAssertEquals("Bundle double array index 0 has correct value", 1.5, doubleArray[0]); + fAssertEquals("Bundle double array index 1 has correct value", 2.5, doubleArray[1]); + + final String[] stringArray = bundle.getStringArray("stringArray"); + fAssertNotNull("Bundle string array should exist", stringArray); + fAssertEquals("Bundle string array has correct length", 2, stringArray.length); + fAssertEquals("Bundle string array index 0 has correct value", "bar", stringArray[0]); + fAssertEquals("Bundle string array index 1 has correct value", "baz", stringArray[1]); + } + + private void checkJSONObject(final JSONObject object) throws JSONException { + fAssertEquals("JSON boolean has correct value", true, object.getBoolean("boolean")); + fAssertEquals("JSON int has correct value", 1, object.getInt("int")); + fAssertEquals("JSON double has correct value", 0.5, object.getDouble("double")); + fAssertEquals("JSON string has correct value", "foo", object.getString("string")); + + final JSONArray booleanArray = object.getJSONArray("booleanArray"); + fAssertNotNull("JSON boolean array should exist", booleanArray); + fAssertEquals("JSON boolean array has correct length", 2, booleanArray.length()); + fAssertEquals("JSON boolean array index 0 has correct value", + false, booleanArray.getBoolean(0)); + fAssertEquals("JSON boolean array index 1 has correct value", + true, booleanArray.getBoolean(1)); + + final JSONArray intArray = object.getJSONArray("intArray"); + fAssertNotNull("JSON int array should exist", intArray); + fAssertEquals("JSON int array has correct length", 2, intArray.length()); + fAssertEquals("JSON int array index 0 has correct value", + 2, intArray.getInt(0)); + fAssertEquals("JSON int array index 1 has correct value", + 3, intArray.getInt(1)); + + final JSONArray doubleArray = object.getJSONArray("doubleArray"); + fAssertNotNull("JSON double array should exist", doubleArray); + fAssertEquals("JSON double array has correct length", 2, doubleArray.length()); + fAssertEquals("JSON double array index 0 has correct value", + 1.5, doubleArray.getDouble(0)); + fAssertEquals("JSON double array index 1 has correct value", + 2.5, doubleArray.getDouble(1)); + + final JSONArray stringArray = object.getJSONArray("stringArray"); + fAssertNotNull("JSON string array should exist", stringArray); + fAssertEquals("JSON string array has correct length", 2, stringArray.length()); + fAssertEquals("JSON string array index 0 has correct value", + "bar", stringArray.getString(0)); + fAssertEquals("JSON string array index 1 has correct value", + "baz", stringArray.getString(1)); + } + + private void checkNativeJSObject(final NativeJSObject object) { + fAssertEquals("Native boolean has correct value", + true, object.getBoolean("boolean")); + fAssertEquals("optBoolean returns existent value", + true, object.optBoolean("boolean", false)); + fAssertEquals("optBoolean returns fallback value if nonexistent", + false, object.optBoolean("nonexistent_boolean", false)); + + fAssertEquals("Native int has correct value", + 1, object.getInt("int")); + fAssertEquals("optInt returns existent value", + 1, object.optInt("int", 0)); + fAssertEquals("optInt returns fallback value if nonexistent", + 0, object.optInt("nonexistent_int", 0)); + + fAssertEquals("Native double has correct value", + 0.5, object.getDouble("double")); + fAssertEquals("optDouble returns existent value", + 0.5, object.optDouble("double", -0.5)); + fAssertEquals("optDouble returns fallback value if nonexistent", + -0.5, object.optDouble("nonexistent_double", -0.5)); + + fAssertEquals("Native string has correct value", + "foo", object.getString("string")); + fAssertEquals("optDouble returns existent value", + "foo", object.optString("string", "bar")); + fAssertEquals("optDouble returns fallback value if nonexistent", + "bar", object.optString("nonexistent_string", "bar")); + + final boolean[] booleanArray = object.getBooleanArray("booleanArray"); + fAssertNotNull("Native boolean array should exist", booleanArray); + fAssertEquals("Native boolean array has correct length", 2, booleanArray.length); + fAssertEquals("Native boolean array index 0 has correct value", false, booleanArray[0]); + fAssertEquals("Native boolean array index 1 has correct value", true, booleanArray[1]); + fAssertNotSame("optBooleanArray returns existent value", + null, object.optBooleanArray("booleanArray", null)); + fAssertSame("optBooleanArray returns fallback value if nonexistent", + null, object.optBooleanArray("nonexistent_booleanArray", null)); + + final int[] intArray = object.getIntArray("intArray"); + fAssertNotNull("Native int array should exist", intArray); + fAssertEquals("Native int array has correct length", 2, intArray.length); + fAssertEquals("Native int array index 0 has correct value", 2, intArray[0]); + fAssertEquals("Native int array index 1 has correct value", 3, intArray[1]); + fAssertNotSame("optIntArray returns existent value", + null, object.optIntArray("intArray", null)); + fAssertSame("optIntArray returns fallback value if nonexistent", + null, object.optIntArray("nonexistent_intArray", null)); + + final double[] doubleArray = object.getDoubleArray("doubleArray"); + fAssertNotNull("Native double array should exist", doubleArray); + fAssertEquals("Native double array has correct length", 2, doubleArray.length); + fAssertEquals("Native double array index 0 has correct value", 1.5, doubleArray[0]); + fAssertEquals("Native double array index 1 has correct value", 2.5, doubleArray[1]); + fAssertNotSame("optDoubleArray returns existent value", + null, object.optDoubleArray("doubleArray", null)); + fAssertSame("optDoubleArray returns fallback value if nonexistent", + null, object.optDoubleArray("nonexistent_doubleArray", null)); + + final String[] stringArray = object.getStringArray("stringArray"); + fAssertNotNull("Native string array should exist", stringArray); + fAssertEquals("Native string array has correct length", 2, stringArray.length); + fAssertEquals("Native string array index 0 has correct value", "bar", stringArray[0]); + fAssertEquals("Native string array index 1 has correct value", "baz", stringArray[1]); + fAssertNotSame("optStringArray returns existent value", + null, object.optStringArray("stringArray", null)); + fAssertSame("optStringArray returns fallback value if nonexistent", + null, object.optStringArray("nonexistent_stringArray", null)); + + fAssertEquals("Native has(null) is false", false, object.has("null")); + fAssertEquals("Native has(emptyString) is true", true, object.has("emptyString")); + + fAssertEquals("Native optBoolean returns fallback value if null", + true, object.optBoolean("null", true)); + fAssertEquals("Native optInt returns fallback value if null", + 42, object.optInt("null", 42)); + fAssertEquals("Native optDouble returns fallback value if null", + -3.1415926535, object.optDouble("null", -3.1415926535)); + fAssertEquals("Native optString returns fallback value if null", + "baz", object.optString("null", "baz")); + + fAssertNotEquals("Native optString does not return fallback value if emptyString", + "baz", object.optString("emptyString", "baz")); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java new file mode 100644 index 000000000..c613eca8f --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoApp; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.util.GeckoEventListener; + +import org.json.JSONException; +import org.json.JSONObject; + +public class testFilePicker extends JavascriptTest implements GeckoEventListener { + private static final String TEST_FILENAME = "/mnt/sdcard/my-favorite-martian.png"; + + public testFilePicker() { + super("testFilePicker.js"); + } + + @Override + public void handleMessage(String event, final JSONObject message) { + // We handle the FilePicker message here so we can send back hard coded file information. We + // don't want to try to emulate "picking" a file using the Android intent chooser. + if (event.equals("FilePicker:Show")) { + try { + message.put("file", TEST_FILENAME); + } catch (JSONException ex) { + fFail("Can't add filename to message " + TEST_FILENAME); + } + + mActions.sendGeckoEvent("FilePicker:Result", message.toString()); + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + GeckoApp.getEventDispatcher().registerGeckoThreadListener(this, "FilePicker:Show"); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + + GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this, "FilePicker:Show"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java new file mode 100644 index 000000000..3c57b864a --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilterOpenTab.java @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.PrivateTab; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.TabsProvider; + +import android.content.ContentProvider; +import android.content.Context; +import android.database.Cursor; + +/** + * Tests that local tabs are filtered prior to upload. + * - create a set of tabs and persists them through TabsAccessor. + * - verifies that tabs are filtered by querying. + */ +public class testFilterOpenTab extends ContentProviderTest { + private static final String[] TABS_PROJECTION_COLUMNS = new String[] { + BrowserContract.Tabs.TITLE, + BrowserContract.Tabs.URL, + BrowserContract.Clients.GUID, + BrowserContract.Clients.NAME + }; + + private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL"; + + /** + * Factory function that makes new ContentProvider instances. + * <p> + * We want a fresh provider each test, so this should be invoked in + * <code>setUp</code> before each individual test. + */ + protected static Callable<ContentProvider> sTabProviderCallable = new Callable<ContentProvider>() { + @Override + public ContentProvider call() { + return new TabsProvider(); + } + }; + + private Cursor getTabsFromLocalClient() throws Exception { + return mProvider.query(BrowserContract.Tabs.CONTENT_URI, + TABS_PROJECTION_COLUMNS, + LOCAL_TABS_SELECTION, + null, + null); + } + + private Tab createTab(int id, String url, boolean external, int parentId, String title) { + return new Tab((Context) getActivity(), id, url, external, parentId, title); + } + + private Tab createPrivateTab(int id, String url, boolean external, int parentId, String title) { + return new PrivateTab((Context) getActivity(), id, url, external, parentId, title); + } + + @Override + public void setUp() throws Exception { + super.setUp(sTabProviderCallable, BrowserContract.TABS_AUTHORITY, "tabs.db"); + mTests.add(new TestInsertLocalTabs()); + } + + public void testFilterOpenTab() throws Exception { + blockForGeckoReady(); + + for (int i = 0; i < mTests.size(); i++) { + Runnable test = mTests.get(i); + + setTestName(test.getClass().getSimpleName()); + test.run(); + } + } + + private class TestInsertLocalTabs extends TestCase { + @Override + public void test() throws Exception { + final String TITLE1 = "Google"; + final String URL1 = "http://www.google.com/"; + final String TITLE2 = "Mozilla Start Page"; + final String URL2 = "about:home"; + final String TITLE3 = "Chrome Weave URL"; + final String URL3 = "chrome://weave/"; + final String TITLE4 = "What You Cache Is What You Get"; + final String URL4 = "wyciwyg://1/test.com"; + final String TITLE5 = "Root Folder"; + final String URL5 = "file:///"; + + // Create a list of local tabs. + List<Tab> tabs = new ArrayList<Tab>(6); + Tab tab1 = createTab(1, URL1, false, 0, TITLE1); + Tab tab2 = createTab(2, URL2, false, 0, TITLE2); + Tab tab3 = createTab(3, URL3, false, 0, TITLE3); + Tab tab4 = createTab(4, URL4, false, 0, TITLE4); + Tab tab5 = createTab(5, URL5, false, 0, TITLE5); + Tab tab6 = createPrivateTab(6, URL1, false, 0, TITLE1); + tabs.add(tab1); + tabs.add(tab2); + tabs.add(tab3); + tabs.add(tab4); + tabs.add(tab5); + tabs.add(tab6); + + // Persist the created tabs. Normally, you should be careful that you get a profile on the + // original thread, and do the work in a background one, but for testing we don't. + final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter); + helper.getProfileDB().getTabsAccessor().persistLocalTabs(mResolver, tabs); + + // Get the persisted tab and check if urls are filtered. + Cursor c = getTabsFromLocalClient(); + assertCountIsAndClose(c, 1, 1 + " tabs entries found"); + } + } + + /** + * Assert that the provided cursor has the expected number of rows, + * closing the cursor afterwards. + */ + private void assertCountIsAndClose(Cursor c, int expectedCount, String message) { + try { + mAsserter.is(c.getCount(), expectedCount, message); + } finally { + c.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java new file mode 100644 index 000000000..2797fdf5b --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFindInPage.java @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Element; +import org.mozilla.gecko.R; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.util.GeckoEventListener; + +import org.json.JSONObject; + +import com.robotium.solo.Condition; + +public class testFindInPage extends JavascriptTest implements GeckoEventListener { + private static final int WAIT_FOR_CONDITION_MS = 3000; + + protected Element next, close; + + public testFindInPage() { + super("testFindInPage.js"); + } + + @Override + public void handleMessage(String event, final JSONObject message) { + if (event.equals("Test:FindInPage")) { + try { + final String text = message.getString("text"); + final int nrOfMatches = Integer.parseInt(message.getString("nrOfMatches")); + findText(text, nrOfMatches); + } catch (Exception e) { + fFail("Can't extract find query from JSON"); + } + } + + if (event.equals("Test:CloseFindInPage")) { + try { + close.click(); + } catch (Exception e) { + fFail("FindInPage prompt not opened"); + } + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + EventDispatcher.getInstance().registerGeckoThreadListener(this, + "Test:FindInPage", + "Test:CloseFindInPage"); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, + "Test:FindInPage", + "Test:CloseFindInPage"); + } + + public void findText(String text, int nrOfMatches){ + selectMenuItem(mStringHelper.FIND_IN_PAGE_LABEL); + close = mDriver.findElement(getActivity(), R.id.find_close); + boolean success = waitForCondition ( new Condition() { + @Override + public boolean isSatisfied() { + next = mDriver.findElement(getActivity(), R.id.find_next); + if (next != null) { + return true; + } else { + return false; + } + } + }, WAIT_FOR_CONDITION_MS); + mAsserter.ok(success, "Looking for the next search match button in the Find in Page UI", "Found the next match button"); + + // TODO: Find a better way to wait and then enter the text + // Without the sleep this seems to work but the actions are not updated in the UI + mSolo.sleep(500); + + mActions.sendKeys(text); + mActions.sendSpecialKey(Actions.SpecialKey.ENTER); + + // Advance a few matches to scroll the page + for (int i=1;i < nrOfMatches;i++) { + success = waitForCondition ( new Condition() { + @Override + public boolean isSatisfied() { + if (next.click()) { + return true; + } else { + return false; + } + } + }, WAIT_FOR_CONDITION_MS); + mSolo.sleep(500); // TODO: Find a better way to wait here because waitForCondition is not enough + mAsserter.ok(success, "Checking if the next button was clicked", "button was clicked"); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java new file mode 100644 index 000000000..e173a8c16 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFlingCorrectness.java @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.PaintedSurface; + +/** + * Basic fling correctness test. + * - Loads a page and verifies it draws + * - Drags page upwards by 200 pixels to get ready for a fling + * - Fling the page downwards so we get back to the top and verify. + */ +public class testFlingCorrectness extends PixelTest { + public void testFlingCorrectness() { + String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL); + + MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop()); + + blockForGeckoReady(); + + // load page and check we're at 0,0 + loadAndVerifyBoxes(url); + + // drag page upwards by 200 pixels (use two drags instead of one in case + // the screen size is small) + Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint(); + meh.dragSync(10, 150, 10, 50); + meh.dragSync(10, 150, 10, 50); + PaintedSurface painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + checkScrollWithBoxes(painted, 0, 200); + } finally { + painted.close(); + } + + // now fling page downwards using a 100-pixel drag but a velocity of 15px/sec, so that + // we scroll the full 200 pixels back to the top of the page + paintExpecter = mActions.expectPaint(); + meh.flingSync(10, 50, 10, 150, 15); + painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + checkScrollWithBoxes(painted, 0, 0); + } finally { + painted.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java new file mode 100644 index 000000000..40968b9be --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFormHistory.java @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; + +import org.mozilla.gecko.db.BrowserContract.FormHistory; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; + +/** + * A basic form history contentprovider test. + * - inserts an element in form history when it is not yet set up + * - inserts an element in form history + * - updates an element in form history + * - deletes an element in form history + */ +public class testFormHistory extends BaseTest { + private static final String DB_NAME = "formhistory.sqlite"; + + public void testFormHistory() { + Context context = (Context)getActivity(); + ContentResolver cr = context.getContentResolver(); + ContentValues[] cvs = new ContentValues[1]; + cvs[0] = new ContentValues(); + + blockForGeckoReady(); + + Uri formHistoryUri; + Uri insertUri; + Uri expectedUri; + int numUpdated; + int numDeleted; + + cvs[0].put("fieldname", "fieldname"); + cvs[0].put("value", "value"); + cvs[0].put("timesUsed", "0"); + cvs[0].put("guid", "guid"); + + // Attempt to insert into the db + formHistoryUri = FormHistory.CONTENT_URI; + Uri.Builder builder = formHistoryUri.buildUpon(); + formHistoryUri = builder.appendQueryParameter("profilePath", mProfile).build(); + + insertUri = cr.insert(formHistoryUri, cvs[0]); + expectedUri = formHistoryUri.buildUpon().appendPath("1").build(); + mAsserter.is(expectedUri.toString(), insertUri.toString(), "Insert returned correct uri"); + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + + cvs[0].put("fieldname", "fieldname2"); + cvs[0].putNull("guid"); + + numUpdated = cr.update(formHistoryUri, cvs[0], null, null); + mAsserter.is(1, numUpdated, "Correct number updated"); + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + + numDeleted = cr.delete(formHistoryUri, null, null); + mAsserter.is(1, numDeleted, "Correct number deleted"); + cvs = new ContentValues[0]; + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + + cvs = new ContentValues[1]; + cvs[0] = new ContentValues(); + cvs[0].put("fieldname", "fieldname"); + cvs[0].put("value", "value"); + cvs[0].put("timesUsed", "0"); + cvs[0].putNull("guid"); + + insertUri = cr.insert(formHistoryUri, cvs[0]); + expectedUri = formHistoryUri.buildUpon().appendPath("1").build(); + mAsserter.is(expectedUri.toString(), insertUri.toString(), "Insert returned correct uri"); + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + + cvs[0].put("guid", "guid"); + + numUpdated = cr.update(formHistoryUri, cvs[0], null, null); + mAsserter.is(1, numUpdated, "Correct number updated"); + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + + numDeleted = cr.delete(formHistoryUri, null, null); + mAsserter.is(1, numDeleted, "Correct number deleted"); + cvs = new ContentValues[0]; + SqliteCompare(DB_NAME, "SELECT * FROM moz_formhistory", cvs); + } + + @Override + public void tearDown() throws Exception { + // remove the entire signons.sqlite file + File profile = new File(mProfile); + File db = new File(profile, "formhistory.sqlite"); + if (db.delete()) { + mAsserter.dumpLog("tearDown deleted "+db.toString()); + } else { + mAsserter.dumpLog("tearDown did not delete "+db.toString()); + } + + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java new file mode 100644 index 000000000..eb9a705be --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoProfile.java @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; +import java.util.Enumeration; +import java.util.Hashtable; + +import org.mozilla.gecko.GeckoApp; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.GeckoProfileDirectories; +import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.util.INIParser; +import org.mozilla.gecko.util.INISection; + +import android.content.Context; +import android.text.TextUtils; + +/** + * This patch tests GeckoProfile. It has unit tests for basic getting and removing of profiles, as well as + * some guest mode tests. It does not test locking and unlocking profiles yet. It does not test the file management in GeckoProfile. + */ + +public class testGeckoProfile extends PixelTest { + private final String TEST_PROFILE_NAME = "testProfile"; + private File mozDir; + public void testGeckoProfile() { + blockForGeckoReady(); + + try { + mozDir = GeckoProfileDirectories.getMozillaDirectory(getActivity()); + } catch(Exception ex) { + // If we can't get the moz dir, something is wrong. Just fail quickly. + mAsserter.ok(false, "Couldn't get moz dir", ex.toString()); + return; + } + + checkProfileCreationDeletion(); + checkGuestProfile(); + } + + // This getter just passes an activity. Passing null should throw. + private void checkDefaultGetter() { + // "Default" is a custom profile set up by the test harness. + mAsserter.info("Test using the test profile", GeckoProfile.CUSTOM_PROFILE); + GeckoProfile profile = GeckoProfile.get(getActivity()); + verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, ((GeckoApp) getActivity()).getProfile().getDir(), true); + + try { + profile = GeckoProfile.get(null); + mAsserter.ok(false, "Passing a null context should throw", profile.toString()); + } catch(Exception ex) { + mAsserter.ok(true, "Passing a null context should throw", ex.toString()); + } + } + + // Test get(Context, String) methods + private void checkNamedGetter(String name) { + mAsserter.info("Test using a named profile", name); + GeckoProfile profile = GeckoProfile.get(getActivity(), name); + if (name != null) { + verifyProfile(profile, name, findDir(name), false); + removeProfile(profile, true); + } else { + // Passing in null for a profile name, should get you the default + File defaultProfile = ((GeckoApp) getActivity()).getProfile().getDir(); + verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, defaultProfile, true); + } + } + + // Test get(Context, String, String) methods + private void checkNameAndPathGetter(String name, boolean createBefore) { + if (name == null) { + checkNameAndPathGetter(name, null, createBefore); + } else { + checkNameAndPathGetter(name, name + "_FORCED_DIR", createBefore); + } + } + + // Test get(Context, String, String) methods + private void checkNameAndPathGetter(String name, String path, boolean createBefore) { + mAsserter.info("Test using a named profile and path", name + ", " + path); + checkNameAndDirGetter(name, /* useFile */ false, path, /* file */ null, createBefore); + } + + private void checkNameAndFileGetter(String name, boolean createBefore) { + if (name == null) { + checkNameAndFileGetter(name, null, createBefore); + } else { + checkNameAndFileGetter(name, new File(mozDir, name + "_FORCED_DIR"), createBefore); + } + } + + private void checkNameAndFileGetter(String name, File f, boolean createBefore) { + mAsserter.info("Test using a named profile and File", name + ", " + f); + checkNameAndDirGetter(name, /* useFile */ true, /* path */ null, f, createBefore); + } + + private void checkNameAndDirGetter(final String name, final boolean useFile, + String path, final File file, + final boolean createBefore) { + final File f; + if (useFile) { + f = file; + } else if (!TextUtils.isEmpty(path)) { + f = new File(mozDir, path); + path = f.getAbsolutePath(); + } else { + f = null; + } + + if (f != null && createBefore) { + // For some tests we create explicitly beforehand + f.mkdir(); + } + + final File testProfileDir = ((GeckoApp) getActivity()).getProfile().getDir(); + final String expectedName = name != null ? name : GeckoProfile.CUSTOM_PROFILE; + + final GeckoProfile profile; + if (useFile) { + profile = GeckoProfile.get(getActivity(), name, file); + } else { + profile = GeckoProfile.get(getActivity(), name, path); + } + + if (name != null || f != null) { + // GeckoProfile will create a directory and add an ini section if f is null + // here. Therefore, when f is null, shouldHaveFound is false for the + // verifyProfile call, and inProfileIni is true for the removeProfile call. + verifyProfile(profile, expectedName, f, f != null); + removeProfile(profile, f == null); + if (name == null) { + // A side effect of calling GeckoProfile.get with null name is it changes + // the test profile's directory to the new directory. Restore it back. + GeckoProfile.get(getActivity(), null, testProfileDir); + mAsserter.is(GeckoProfile.get(getActivity()).getDir(), testProfileDir, + "Test profile should be restored"); + } + } else { + // Passing in null for a profile name and path, should get you the default + verifyProfile(profile, expectedName, testProfileDir, true); + } + } + + private void checkProfileCreationDeletion() { + // Test + checkDefaultGetter(); + + int index = 0; + checkNamedGetter(TEST_PROFILE_NAME + (index++)); // 0 + checkNamedGetter(null); + + // name and path + checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), true); + checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), false); + checkNameAndPathGetter(null, false); + // null name and path + checkNameAndPathGetter(null, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR", true); + checkNameAndPathGetter(null, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR", false); + // name and null path + checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), null, false); + checkNameAndPathGetter(TEST_PROFILE_NAME + (index++), "", false); + // null name and null path + checkNameAndPathGetter(null, null, false); + checkNameAndPathGetter(null, "", false); + + // name and path + checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), true); + checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), false); + checkNameAndFileGetter(null, false); + // null name and path + checkNameAndFileGetter(null, new File(mozDir, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR"), true); + checkNameAndFileGetter(null, new File(mozDir, TEST_PROFILE_NAME + (index++) + "_FORCED_DIR"), false); + // name and null path + checkNameAndFileGetter(TEST_PROFILE_NAME + (index++), null, false); + // null name and null path + checkNameAndFileGetter(null, null, false); + } + + // Tests of Guest profile methods + private void checkGuestProfile() { + final File testProfileDir = ((GeckoApp) getActivity()).getProfile().getDir(); + + mAsserter.info("Test getting a guest profile", ""); + GeckoProfile profile = GeckoProfile.getGuestProfile(getActivity()); + verifyProfile(profile, GeckoProfile.CUSTOM_PROFILE, getActivity().getFileStreamPath("guest"), true); + mAsserter.ok(profile.inGuestMode(), "Profile is in guest mode", profile.getName()); + + final File dir = profile.getDir(); + mAsserter.info("Test deleting a guest profile", ""); + mAsserter.ok(GeckoProfile.removeProfile(getActivity(), profile), "Cleaned up unlocked guest profile", profile.getName()); + mAsserter.ok(!dir.exists(), "Guest dir was deleted", dir.toString()); + + // Restore test profile directory, which was changed in the last GeckoProfile.get call. + GeckoProfile.get(getActivity(), null, testProfileDir); + mAsserter.is(GeckoProfile.get(getActivity()).getDir(), testProfileDir, + "Test profile should be restored"); + } + + // Runs generic tests on a profile to make sure it looks correct + private void verifyProfile(GeckoProfile profile, String name, File requestedDir, boolean shouldHaveFound) { + mAsserter.is(profile.getName(), name, "Profile name is correct"); + + File dir = null; + if (!shouldHaveFound) { + mAsserter.is(findDir(name), null, "Dir with name doesn't exist yet"); + + dir = profile.getDir(); + mAsserter.isnot(requestedDir, dir, "Profile should not have used expectedDir"); + + // The used dir should be based on the name passed in. + requestedDir = findDir(name); + } else { + dir = profile.getDir(); + } + + mAsserter.is(dir, requestedDir, "Profile dir is correct"); + mAsserter.ok(dir.exists(), "Profile dir exists after getting it", dir.toString()); + } + + // Tries to find a profile in profiles.ini. Makes sure its name and path match what is expected + private void findInProfilesIni(final String name, final File dir, final boolean shouldFind) { + final File mozDir; + try { + mozDir = GeckoProfileDirectories.getMozillaDirectory(getActivity()); + } catch(Exception ex) { + mAsserter.ok(false, "Couldn't get moz dir", ex.toString()); + return; + } + + final INIParser parser = GeckoProfileDirectories.getProfilesINI(mozDir); + final Hashtable<String, INISection> sections = parser.getSections(); + + boolean found = false; + for (Enumeration<INISection> e = sections.elements(); e.hasMoreElements();) { + final INISection section = e.nextElement(); + String iniName = section.getStringProperty("Name"); + if (iniName == null || !iniName.equals(name)) { + continue; + } + + found = true; + + String iniPath = section.getStringProperty("Path"); + mAsserter.is(name, iniName, "Section with name found"); + mAsserter.is(dir.getName(), iniPath, "Section has correct path"); + } + + mAsserter.is(found, shouldFind, "Found profile where expected"); + } + + // Tries to remove a profile from Gecko profile. Verifies that it's removed from profiles.ini and its directory is deleted. + // TODO: Reconsider profile removal. Firefox would not normally remove a + // profile. Outstanding tasks may still try to access files in the profile. + private void removeProfile(GeckoProfile profile, boolean inProfilesIni) { + final String name = profile.getName(); + final File dir = profile.getDir(); + findInProfilesIni(name, dir, inProfilesIni); + mAsserter.ok(dir.exists(), "Profile dir exists before removing", dir.toString()); + mAsserter.ok(GeckoProfile.removeProfile(getActivity(), profile), "Remove was successful", name); + mAsserter.ok(!dir.exists(), "Profile dir was deleted when it was removed", dir.toString()); + findInProfilesIni(name, dir, false); + } + + // Looks for a dir whose name ends with the passed-in string. + private File findDir(String name) { + final File root; + try { + root = GeckoProfileDirectories.getMozillaDirectory(getActivity()); + } catch(Exception ex) { + return null; + } + + File[] dirs = root.listFiles(); + for (File dir : dirs) { + if (dir.getName().endsWith(name)) { + return dir; + } + } + + return null; + } + + @Override + public void tearDown() throws Exception { + // Clear SharedPreferences. + final Context context = getInstrumentation().getContext(); + GeckoSharedPrefs.forProfile(context).edit().clear().apply(); + + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java new file mode 100644 index 000000000..ac4a9862c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGeckoRequest.java @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.concurrent.atomic.AtomicBoolean; + +import com.robotium.solo.Condition; + +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.tests.helpers.AssertionHelper; +import org.mozilla.gecko.tests.helpers.WaitHelper; +import org.mozilla.gecko.util.GeckoRequest; +import org.mozilla.gecko.util.NativeJSObject; + +/** + * Tests sending and receiving Gecko requests using the GeckoRequest API. + */ +public class testGeckoRequest extends JavascriptBridgeTest { + private static final String TEST_JS = "testGeckoRequest.js"; + private static final String REQUEST_EVENT = "Robocop:GeckoRequest"; + private static final String REQUEST_EXCEPTION_EVENT = "Robocop:GeckoRequestException"; + private static final int MAX_WAIT_MS = 5000; + + public void testGeckoRequest() { + blockForReadyAndLoadJS(TEST_JS); + + // Register a listener for this request. + getJS().syncCall("add_request_listener", REQUEST_EVENT); + + // Make sure we receive the expected response. + checkFooRequest(); + + // Try registering a second listener for this request, which should fail. + getJS().syncCall("add_second_request_listener", REQUEST_EVENT); + + // Unregister the listener for this request. + getJS().syncCall("remove_request_listener", REQUEST_EVENT); + + // Make sure we don't receive a response after removing the listener. + checkUnregisteredRequest(); + + // Check that we still receive a response for listeners that throw. + getJS().syncCall("add_exception_listener", REQUEST_EXCEPTION_EVENT); + checkExceptionRequest(); + getJS().syncCall("remove_request_listener", REQUEST_EXCEPTION_EVENT); + + getJS().syncCall("finish_test"); + } + + private void checkFooRequest() { + final AtomicBoolean responseReceived = new AtomicBoolean(false); + final String data = "foo"; + + GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EVENT, data) { + @Override + public void onResponse(NativeJSObject nativeJSObject) { + // Ensure we receive the expected response from Gecko. + final String result = nativeJSObject.getString("result"); + AssertionHelper.fAssertEquals("Sent and received request data", data + "bar", result); + responseReceived.set(true); + } + }); + + WaitHelper.waitFor("Received response for registered listener", new Condition() { + @Override + public boolean isSatisfied() { + return responseReceived.get(); + } + }, MAX_WAIT_MS); + } + + private void checkExceptionRequest() { + final AtomicBoolean responseReceived = new AtomicBoolean(false); + final AtomicBoolean errorReceived = new AtomicBoolean(false); + + GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EXCEPTION_EVENT, null) { + @Override + public void onResponse(NativeJSObject nativeJSObject) { + responseReceived.set(true); + } + + @Override + public void onError(NativeJSObject error) { + errorReceived.set(true); + } + }); + + WaitHelper.waitFor("Received error for listener with exception", new Condition() { + @Override + public boolean isSatisfied() { + return errorReceived.get(); + } + }, MAX_WAIT_MS); + + AssertionHelper.fAssertTrue("onResponse not called for listener with exception", !responseReceived.get()); + } + + private void checkUnregisteredRequest() { + final AtomicBoolean responseReceived = new AtomicBoolean(false); + + GeckoAppShell.sendRequestToGecko(new GeckoRequest(REQUEST_EVENT, null) { + @Override + public void onResponse(NativeJSObject nativeJSObject) { + responseReceived.set(true); + } + }); + + // This check makes sure that we do *not* receive a response for an unregistered listener, + // meaning waitForCondition() should always time out. + getSolo().waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return responseReceived.get(); + } + }, MAX_WAIT_MS); + + AssertionHelper.fAssertTrue("Did not receive response for unregistered listener", !responseReceived.get()); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java new file mode 100644 index 000000000..405ddef7a --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testGetUserMedia.java @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.AppConstants; + +import android.widget.Spinner; +import android.view.View; + +import com.robotium.solo.Condition; + +import android.hardware.Camera; +import android.os.Build; + +public class testGetUserMedia extends BaseTest { + private static final String LOGTAG = testGetUserMedia.class.getSimpleName(); + + private static final String GUM_MESSAGE = "Would you like to share your camera and microphone with"; + private static final String GUM_ALLOW = "^Share$"; + private static final String GUM_DENY = "^Don't Share$"; + + private static final String GUM_BACK_CAMERA = "Back facing camera"; + private static final String GUM_SELECT_TAB = "Choose a tab to stream"; + + private static final String GUM_PAGE_TITLE = "gUM Test Page"; + private static final String GUM_PAGE_FAILED = "failed gumtest"; + private static final String GUM_PAGE_AUDIO = "audio gumtest"; + private static final String GUM_PAGE_VIDEO = "video gumtest"; + private static final String GUM_PAGE_AUDIOVIDEO = "audiovideo gumtest"; + + public void testGetUserMedia() { + // TabShare.js is disabled on release builds. + if (AppConstants.RELEASE_OR_BETA) { + mAsserter.dumpLog(LOGTAG + " is disabled on release builds: returning"); + return; + } + + // Only try GUM test if the device has a camera (emulation). + if (Camera.getNumberOfCameras() <= 0) { + return; + } + + blockForGeckoReady(); + + final String GUM_CAMERA_URL = getAbsoluteUrl("/robocop/robocop_getusermedia2.html"); + final String GUM_TAB_URL = getAbsoluteUrl("/robocop/robocop_getusermedia.html"); + // Browser constraint needs HTTPS + final String GUM_TAB_HTTPS_URL = GUM_TAB_URL.replace("http://mochi.test:8888", "https://example.com"); + + // Tests on Camera page will test camera enumeration code, but + // the actual cameras don't seem to work on the emulators, so + // the enumeration is all that gets tested. + + // Test GUM notification showing + loadUrlAndWait(GUM_CAMERA_URL); + waitForText(GUM_MESSAGE); + mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed"); + waitForSpinner(); + // At least one camera detected + mAsserter.is(mSolo.searchText(GUM_BACK_CAMERA), true, "getUserMedia found a camera"); + mSolo.clickOnButton(GUM_DENY); + waitForTextDismissed(GUM_MESSAGE); + mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal"); + verifyUrlBarTitle(GUM_CAMERA_URL); + + // Cameras don't work on the testing hardware, so stream a tab + loadUrlAndWait(GUM_TAB_HTTPS_URL); + waitForText(GUM_MESSAGE); + mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed"); + waitForSpinner(); + mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available"); + mAsserter.is(mSolo.searchText("MICROPHONE TO USE"), true, "Microphone selection available"); + mAsserter.is(mSolo.searchText("Microphone 1"), true, "Microphone 1 available"); + mSolo.clickOnText("Microphone 1"); + waitForText("No Audio"); + mAsserter.is(mSolo.searchText("No Audio"), true, "No 'No Audio' selection available"); + mSolo.clickOnText("No Audio"); + waitForTextDismissed("Microphone 1"); + mAsserter.is(mSolo.searchText("Microphone 1"), false, "Audio selection hidden after dismissal"); + mAsserter.is(mSolo.searchText(GUM_ALLOW), true, "Share button available after selection"); + mSolo.clickOnButton(GUM_ALLOW); + waitForTextDismissed(GUM_MESSAGE); + mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal"); + waitForText(GUM_SELECT_TAB); + mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Tab selection dialog displayed"); + mSolo.clickOnText(GUM_PAGE_TITLE); + waitForTextDismissed(GUM_SELECT_TAB); + mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), false, "Tab selection dialog hidden"); + verifyUrlBarTitle(GUM_TAB_HTTPS_URL); + + // Android 2.3 testers fail because of audio issues: + // E/AudioRecord( 650): Unsupported configuration: sampleRate 44100, format 1, channelCount 1 + // E/libOpenSLES( 650): android_audioRecorder_realize(0x26d7d8) error creating AudioRecord object + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return; + } + + loadUrlAndWait(GUM_TAB_HTTPS_URL); + waitForText(GUM_MESSAGE); + mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed"); + + waitForSpinner(); + mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available"); + mSolo.clickOnButton(GUM_ALLOW); + waitForTextDismissed(GUM_MESSAGE); + waitForText(GUM_SELECT_TAB); + mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Tab selection dialog displayed"); + mSolo.clickOnText(GUM_PAGE_TITLE); + waitForTextDismissed(GUM_SELECT_TAB); + mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), false, "Tab selection dialog hidden"); + verifyUrlBarTitle(GUM_TAB_HTTPS_URL); + + loadUrlAndWait(GUM_TAB_HTTPS_URL); + waitForText(GUM_MESSAGE); + mAsserter.is(mSolo.searchText(GUM_MESSAGE), true, "getUserMedia doorhanger has been displayed"); + + waitForSpinner(); + mAsserter.is(mSolo.searchText(GUM_SELECT_TAB), true, "Video source selection available"); + mSolo.clickOnText(GUM_SELECT_TAB); + waitForText("No Video"); + mAsserter.is(mSolo.searchText("No Video"), true, "'No video' source selection available"); + mSolo.clickOnText("No Video"); + waitForTextDismissed(GUM_SELECT_TAB); + mSolo.clickOnButton(GUM_ALLOW); + waitForTextDismissed(GUM_MESSAGE); + mAsserter.is(mSolo.searchText(GUM_MESSAGE), false, "getUserMedia doorhanger hidden after dismissal"); + verifyUrlBarTitle(GUM_TAB_HTTPS_URL); + } + + // wait for a Spinner view that is clickable + private void waitForSpinner() { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + for (Spinner view : mSolo.getCurrentViews(Spinner.class)) { + if (view.isClickable() && + view.getVisibility() == View.VISIBLE && + view.getWidth() > 0 && + view.getHeight() > 0) { + return true; + } + } + return false; + } + }, MAX_WAIT_MS); + } + + // wait until the specified text is *not* displayed + private void waitForTextDismissed(final String text) { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return !mSolo.searchText(text); + } + }, MAX_WAIT_MS); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java new file mode 100644 index 000000000..1f2fbbd38 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistory.java @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import org.mozilla.gecko.home.HomePager; + +import com.robotium.solo.Condition; + +public class testHistory extends AboutHomeTest { + private View mFirstChild; + + public void testHistory() { + blockForGeckoReady(); + + String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + String url2 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + String url3 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL); + + inputAndLoadUrl(url); + verifyUrlBarTitle(url); + inputAndLoadUrl(url2); + verifyUrlBarTitle(url2); + inputAndLoadUrl(url3); + verifyUrlBarTitle(url3); + + openAboutHomeTab(AboutHomeTabs.HISTORY); + + final ListView hList = findListViewWithTag(HomePager.LIST_TAG_HISTORY); + mAsserter.is(waitForNonEmptyListToLoad(hList), true, "list is properly loaded"); + + // Click on the history item and wait for the page to load + // wait for the history list to be populated + mFirstChild = null; + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + mFirstChild = hList.getChildAt(1); + if (mFirstChild == null) { + return false; + } + if (mFirstChild instanceof android.view.ViewGroup) { + ViewGroup group = (ViewGroup)mFirstChild; + if (group.getChildCount() < 1) { + return false; + } + for (int i = 0; i < group.getChildCount(); i++) { + View grandChild = group.getChildAt(i); + if (grandChild instanceof android.widget.TextView) { + mAsserter.ok(true, "found TextView:", ((android.widget.TextView)grandChild).getText().toString()); + } + } + } else { + mAsserter.dumpLog("first child not a ViewGroup: "+mFirstChild); + return false; + } + return true; + } + }, MAX_WAIT_MS); + + mAsserter.isnot(mFirstChild, null, "Got history item"); + mSolo.clickOnView(mFirstChild); + + // The first item here (since it was just visited) should be a "Switch to tab" item + // i.e. don't expect a DOMContentLoaded event + verifyUrlBarTitle(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL); + verifyUrl(url3); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java new file mode 100644 index 000000000..4c605f6c3 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHistoryService.java @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +public class testHistoryService extends JavascriptTest { + + public testHistoryService() { + super("testHistoryService.js"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java new file mode 100644 index 000000000..be36ae5a0 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeBanner.java @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +public class testHomeBanner extends UITest { + + private static final String TEST_URL = "chrome://roboextender/content/robocop_home_banner.html"; + private static final String TEXT = "The quick brown fox jumps over the lazy dog."; + + public void testHomeBanner() { + GeckoHelper.blockForReady(); + + // Make sure the banner is not visible to start. + mAboutHome.assertVisible() + .assertBannerNotVisible(); + + // These test methods depend on being run in this order. + addBannerTest(); + + // Make sure the banner hides when the user starts interacting with the url bar. + hideOnToolbarFocusTest(); + + // Make sure to test dismissing the banner after everything else, since dismissing + // the banner will prevent it from showing up again. + dismissBannerTest(); + } + + /** + * Adds a banner message, verifies that it appears when it should, and verifies that + * onshown/onclick handlers are called in JS. + * + * Note: This test does not remove the message after it is done. + */ + private void addBannerTest() { + // Load about:home and make sure the onshown handler is called. + Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageShown"); + addBannerMessage(); + NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL); + eventExpecter.blockForEvent(); + + // Verify that the banner is visible with the correct text. + mAboutHome.assertBannerText(TEXT); + + // Verify that the banner isn't visible after navigating away from about:home. + NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_FIREFOX_URL); + mAboutHome.assertBannerNotVisible(); + } + + + private void hideOnToolbarFocusTest() { + NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL); + mAboutHome.assertVisible() + .assertBannerVisible(); + + mToolbar.enterEditingMode(); + mAboutHome.assertBannerNotVisible(); + + mToolbar.dismissEditingMode(); + mAboutHome.assertBannerVisible(); + } + + /** + * Adds a banner message, verifies that its ondismiss handler is called in JS, + * and verifies that the banner is no longer shown after it is dismissed. + * + * Note: This test does not remove the message after it is done. + */ + private void dismissBannerTest() { + NavigationHelper.enterAndLoadUrl(mStringHelper.ABOUT_HOME_URL); + mAboutHome.assertVisible(); + + // Test to make sure the ondismiss handler is called when the close button is clicked. + final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageDismissed"); + mAboutHome.dismissBanner(); + eventExpecter.blockForEvent(); + + mAboutHome.assertBannerNotVisible(); + } + + /** + * Loads the roboextender page to add a message to the banner. + */ + private void addBannerMessage() { + final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageAdded"); + NavigationHelper.enterAndLoadUrl(TEST_URL + "#addMessage"); + eventExpecter.blockForEvent(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java new file mode 100644 index 000000000..fbe2df82f --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testHomeListsProvider.java @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +public class testHomeListsProvider extends ContentProviderTest { + // This test does not run, so it just needs to compile. The test was + // disabled at the time the real Contract was removed; to leave a skeleton + // for a future re-implementor, we include this dummy Contract class. + private static class Contract { + public static final Uri CONTENT_URI = null; + public static final Uri CONTENT_FAKE_URI = null; + + public static final String _ID = null; + public static final String PROVIDER_ID = null; + public static final String TITLE = null; + public static final String URL = null; + } + + @SuppressWarnings("unused") + private void ensureEmptyDatabase() throws Exception { + // Delete all the list entries. + mProvider.delete(Contract.CONTENT_URI, null, null); + + final Cursor c = mProvider.query(Contract.CONTENT_URI, null, null, null, null); + mAsserter.is(c.getCount(), 0, "All list entries were deleted"); + c.close(); + } + + @Override + public void setUp() throws Exception { + // This test is disabled, so this just needs to compile. + super.setUp(null, null, "homelists.db"); + + mTests.add(new TestFakeItems()); + + // Disabled until database support lands + //mTests.add(new TestInsertItem()); + } + + public void testListsProvider() throws Exception { + for (int i = 0; i < mTests.size(); i++) { + Runnable test = mTests.get(i); + + setTestName(test.getClass().getSimpleName()); + // Disabled until database support lands + //ensureEmptyDatabase(); + test.run(); + } + } + + abstract class Test implements Runnable { + @Override + public void run() { + try { + test(); + } catch (Exception e) { + mAsserter.is(true, false, "Test " + this.getClass().getName() + + " threw exception: " + e); + } + } + + public abstract void test() throws Exception; + } + + class TestFakeItems extends Test { + @Override + public void test() throws Exception { + final long id = 1; + final String providerId = "fake-provider"; + final String title = "Example"; + final String url = "http://example.com"; + + final Cursor c = mProvider.query(Contract.CONTENT_FAKE_URI, null, null, null, null); + mAsserter.is(c.moveToFirst(), true, "Fake list item found"); + + mAsserter.is(c.getLong(c.getColumnIndex(Contract._ID)), id, "Fake list item has correct ID"); + mAsserter.is(c.getString(c.getColumnIndex(Contract.PROVIDER_ID)), providerId, "Fake list item has correct provider ID"); + mAsserter.is(c.getString(c.getColumnIndex(Contract.TITLE)), title, "Fake list item has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(Contract.URL)), url, "Fake list item has correct URL"); + + c.close(); + } + } + + class TestInsertItem extends Test { + @Override + public void test() throws Exception { + final String providerId = "{c77da387-4c80-0c45-9f22-70276c29b3ed}"; + final String title = "Mozilla"; + final String url = "https://mozilla.org"; + + // Insert a new list item with test values. + final ContentValues cv = new ContentValues(); + cv.put(Contract.PROVIDER_ID, providerId); + cv.put(Contract.TITLE, title); + cv.put(Contract.URL, url); + + final long id = ContentUris.parseId(mProvider.insert(Contract.CONTENT_URI, cv)); + + // Check that the item was inserted correctly. + final Cursor c = mProvider.query(Contract.CONTENT_URI, null, Contract._ID + " = ?", new String[] { String.valueOf(id) }, null); + mAsserter.is(c.moveToFirst(), true, "Inserted list item found"); + + mAsserter.is(c.getString(c.getColumnIndex(Contract.PROVIDER_ID)), providerId, "Inserted list item has correct provider ID"); + mAsserter.is(c.getString(c.getColumnIndex(Contract.TITLE)), title, "Inserted list item has correct title"); + mAsserter.is(c.getString(c.getColumnIndex(Contract.URL)), url, "Inserted list item has correct URL"); + + c.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java new file mode 100644 index 000000000..5cbbd1be9 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testICODecoder.java @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.graphics.Bitmap; + +import org.mozilla.gecko.icons.decoders.ICODecoder; +import org.mozilla.gecko.icons.decoders.IconDirectoryEntry; +import org.mozilla.gecko.icons.decoders.LoadFaviconResult; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +public class testICODecoder extends UITest { + + private int mGolemNumIconDirEntries; + + public void testICODecoder() throws IOException { + testMicrosoftFavicon(); + testNvidiaFavicon(); + testGolemFavicon(); + testMissingHeader(); + testCorruptIconDirectory(); + } + + /** + * Decode and verify a Microsoft favicon with six different sizes: + * 128x128, 72x72, 48x48, 32x32, 24x24, 16x16 + * Each of the six BMPs supposedly has zero colour depth. + */ + private void testMicrosoftFavicon() throws IOException { + byte[] icoBytes = readICO("microsoft_favicon.ico"); + fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length); + + ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0, + icoBytes.length); + LoadFaviconResult result = decoder.decode(); + fAssertNotNull("Expecting Microsoft favicon to not fail decoding.", result); + + int largestBitmap = Integer.MAX_VALUE; + + int[] possibleSizes = {16, 24, 32, 48, 72, 128}; + for (int i = 0; i < possibleSizes.length; i++) { + if (possibleSizes[i] > decoder.getLargestFaviconSize()) { + largestBitmap = possibleSizes[i]; + + // Verify that all bitmaps but the smallest larger than Favicons.largestFaviconSize + // have been discarded. + for (int j = i + 1; j < possibleSizes.length; j++) { + Bitmap selectedBitmap = result.getBestBitmap(possibleSizes[j]); + fAssertNotNull("Expecting a best bitmap to be found for " + + possibleSizes[j] + "x" + possibleSizes[j], selectedBitmap); + + fAssertEquals("Expecting best bitmap to have width " + possibleSizes[i], + possibleSizes[i], selectedBitmap.getWidth()); + fAssertEquals("Expecting best bitmap to have height " + possibleSizes[i], + possibleSizes[i], selectedBitmap.getHeight()); + + // Reset the result's bitmap iterator. + result = decoder.decode(); + } + + break; + } + } + + int[] expectedSizes = { + // If we request a 33x33 we should get a 48x48. + 33, 48, + // If we request a 24x24 we should get a 24x24. + 24, 24, + // If we request a 8x8 we should get a 16x16. + 8, 16, + }; + + for (int i = 0; i < expectedSizes.length - 1; i += 2) { + if (expectedSizes[i + 1] > largestBitmap) { + // This bitmap has been discarded. + continue; + } + + Bitmap selectedBitmap = result.getBestBitmap(expectedSizes[i]); + fAssertNotNull("Expecting a best bitmap to have been found for " + + expectedSizes[i] + "x" + expectedSizes[i], selectedBitmap); + + fAssertEquals("Expecting best bitmap to have width " + expectedSizes[i + 1], + expectedSizes[i + 1], selectedBitmap.getWidth()); + fAssertEquals("Expecting best bitmap to have height " + expectedSizes[i + 1], + expectedSizes[i + 1], selectedBitmap.getHeight()); + + // Reset the result's bitmap iterator. + result = decoder.decode(); + } + } + + /** + * Decode and verify a NVIDIA favicon with three different colour depths, + * and three different sizes for each colour depth. All payloads are BMP. + */ + private void testNvidiaFavicon() throws IOException { + byte[] icoBytes = readICO("nvidia_favicon.ico"); + fAssertEquals("Expecting NVIDIA favicon to be 25214 bytes.", 25214, icoBytes.length); + + ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0, + icoBytes.length); + fAssertNotNull("Expecting NVIDIA favicon to not fail decoding.", decoder.decode()); + + // Verify the best entry is correctly chosen for each width. + // We expect 32 bpp in all cases even if 32 bpp exceeds IconDirectoryEntry.maxBPP. + // This is okay because IconDirectoryEntry.maxBPP is a "desired bpp" not the absolute max. + // This was chosen because we think it gives better results to select a higher bpp and let + // Android downscale the bpp, rather than showing a bitmap of potentially significantly + // lower color depth. + IconDirectoryEntry[] expectedEntries = { + new IconDirectoryEntry(16, 16, 0, 32, 1128, 24086, false), + new IconDirectoryEntry(32, 32, 0, 32, 4264, 19822, false), + new IconDirectoryEntry(48, 48, 0, 32, 9640, 10182, false) + }; + + IconDirectoryEntry[] directory = decoder.getIconDirectory(); + fAssertTrue("NVIDIA icon directory must contain at least one entry.", directory.length > 0); + for (int i = 0; i < directory.length; i++) { + if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) { + // This test-case has been discarded due to being over-sized. Next. + // All subsequent cases will be too. + fAssertTrue("At least one test-case should not have been discarded.", i > 0); + break; + } + + // Verify the actual Icon Directory entry was as expected. + fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i], + 0, directory[i].compareTo(expectedEntries[i])); + } + } + + /** + * Decode and verify a Golem.de favicon with five bitmaps: 256x256, 48x48, 32x32, 24x24, 16x16 + * Only the 256x256 is a PNG payload. All others are BMP. + */ + private void testGolemFavicon() throws IOException { + byte[] icoBytes = readICO("golem_favicon.ico"); + fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length); + + ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0, + icoBytes.length); + fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode()); + + // Verify the five entries were correctly identified. + IconDirectoryEntry[] expectedEntries = { + new IconDirectoryEntry(16, 16, 0, 32, 1128, 39250, false), + new IconDirectoryEntry(24, 24, 0, 32, 2488, 37032, false), + new IconDirectoryEntry(32, 32, 0, 32, 4392, 32640, false), + new IconDirectoryEntry(48, 48, 0, 32, 9832, 22808, false), + new IconDirectoryEntry(256, 256, 0, 32, 22722, 86, true) + }; + + IconDirectoryEntry[] directory = decoder.getIconDirectory(); + fAssertTrue("Golem icon directory must contain at least one entry.", directory.length > 0); + for (int i = 0; i < directory.length; i++) { + if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) { + // This test-case has been discarded due to being over-sized. + // All subsequent cases will be too. + fAssertTrue("At least one test-case should not have been discarded.", i > 0); + break; + } + + // Verify the actual Icon Directory entry was as expected. + fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i], + 0, directory[i].compareTo(expectedEntries[i])); + } + + // How many icon directory entries in the non-maimed favicon? + mGolemNumIconDirEntries = directory.length; + } + + /** + * Verify that deleting the header will make decoding fail. + */ + private void testMissingHeader() throws IOException { + byte[] icoBytes = readICO("microsoft_favicon.ico"); + fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length); + + int offsetNoHeader = ICODecoder.ICO_HEADER_LENGTH_BYTES; + int lenNoHeader = icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES; + ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, + offsetNoHeader, lenNoHeader); + fAssertNull("Expecting Microsoft favicon to fail decoding.", decoder.decode()); + } + + /** + * Verify that decoding does not fail if the number of icon directory entries is smaller than + * the number given in the header. + */ + private void testCorruptIconDirectory() throws IOException { + byte[] icoBytes = readICO("golem_favicon.ico"); + fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length); + + byte[] icoMaimed = new byte[icoBytes.length - ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES]; + // Copy the header and first four icon directory entries into icoMaimed. + System.arraycopy(icoBytes, 0, icoMaimed, 0, + ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES); + // Skip the last icon directory entry. + System.arraycopy(icoBytes, + ICODecoder.ICO_HEADER_LENGTH_BYTES + 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES, + icoMaimed, + ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES, + icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES - 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES); + + ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoMaimed, 0, + icoMaimed.length); + fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode()); + fAssertEquals("Expecting Golem favicon icon directory to contain one less bitmap.", + mGolemNumIconDirEntries - 1, decoder.getIconDirectory().length); + } + + private byte[] readICO(String fileName) throws IOException { + String filePath = "ico_decoder_favicons" + File.separator + fileName; + InputStream icoStream = getInstrumentation().getContext().getAssets().open(filePath); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(icoStream.available()); + + int readByte; + while ((readByte = icoStream.read()) != -1) { + byteStream.write(readByte); + } + + return byteStream.toByteArray(); + } +} + diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java new file mode 100644 index 000000000..f9a6bcef7 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputConnection.java @@ -0,0 +1,349 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.WaitHelper.waitFor; + +import org.mozilla.gecko.tests.components.GeckoViewComponent.InputConnectionTest; +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +import com.robotium.solo.Condition; + +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/** + * Tests the proper operation of GeckoInputConnection + */ +public class testInputConnection extends JavascriptBridgeTest { + + private static final String INITIAL_TEXT = "foo"; + + public void testInputConnection() throws InterruptedException { + GeckoHelper.blockForReady(); + + final String url = mStringHelper.ROBOCOP_INPUT_URL; + NavigationHelper.enterAndLoadUrl(url); + mToolbar.assertTitle(url); + + // First run tests inside the normal input field. + getJS().syncCall("focus_input", INITIAL_TEXT); + mGeckoView.mTextInput + .waitForInputConnection() + .testInputConnection(new BasicInputConnectionTest()); + + // Then switch focus to the text area and rerun tests. + getJS().syncCall("focus_text_area", INITIAL_TEXT); + mGeckoView.mTextInput + .waitForInputConnection() + .testInputConnection(new BasicInputConnectionTest()); + + // Then switch focus to the content editable and rerun tests. + getJS().syncCall("focus_content_editable", INITIAL_TEXT); + mGeckoView.mTextInput + .waitForInputConnection() + .testInputConnection(new BasicInputConnectionTest()); + + // Then switch focus to the design mode document and rerun tests. + getJS().syncCall("focus_design_mode", INITIAL_TEXT); + mGeckoView.mTextInput + .waitForInputConnection() + .testInputConnection(new BasicInputConnectionTest()); + + // Then switch focus to the resetting input field, and run tests there. + getJS().syncCall("focus_resetting_input", ""); + mGeckoView.mTextInput + .waitForInputConnection() + .testInputConnection(new ResettingInputConnectionTest()); + + // Then switch focus to the hiding input field, and run tests there. + getJS().syncCall("focus_hiding_input", ""); + mGeckoView.mTextInput + .waitForInputConnection() + .testInputConnection(new HidingInputConnectionTest()); + + getJS().syncCall("finish_test"); + } + + private class BasicInputConnectionTest extends InputConnectionTest { + @Override + public void test(final InputConnection ic, EditorInfo info) { + waitFor("focus change", new Condition() { + @Override + public boolean isSatisfied() { + return INITIAL_TEXT.equals(getText(ic)); + } + }); + + // Test setSelection + ic.setSelection(0, 3); + assertSelection("Can set selection to range", ic, 0, 3); + ic.setSelection(-3, 6); + // Test both forms of assert + assertTextAndSelection("Can handle invalid range", ic, INITIAL_TEXT, 0, 3); + ic.setSelection(3, 3); + assertSelectionAt("Can collapse selection", ic, 3); + ic.setSelection(4, 4); + assertTextAndSelectionAt("Can handle invalid cursor", ic, INITIAL_TEXT, 3); + + // Test commitText + ic.commitText("", 10); // Selection past end of new text + assertTextAndSelectionAt("Can commit empty text", ic, "foo", 3); + ic.commitText("bar", 1); // Selection at end of new text + assertTextAndSelectionAt("Can commit text (select after)", ic, "foobar", 6); + ic.commitText("foo", -1); // Selection at start of new text + assertTextAndSelectionAt("Can commit text (select before)", ic, "foobarfoo", 5); + + // Test deleteSurroundingText + ic.deleteSurroundingText(1, 0); + assertTextAndSelectionAt("Can delete text before", ic, "foobrfoo", 4); + ic.deleteSurroundingText(1, 1); + assertTextAndSelectionAt("Can delete text before/after", ic, "foofoo", 3); + ic.deleteSurroundingText(0, 10); + assertTextAndSelectionAt("Can delete text after", ic, "foo", 3); + ic.deleteSurroundingText(0, 0); + assertTextAndSelectionAt("Can delete empty text", ic, "foo", 3); + + // Test setComposingText + ic.setComposingText("foo", 1); + assertTextAndSelectionAt("Can start composition", ic, "foofoo", 6); + ic.setComposingText("", 1); + assertTextAndSelectionAt("Can set empty composition", ic, "foo", 3); + ic.setComposingText("bar", 1); + assertTextAndSelectionAt("Can update composition", ic, "foobar", 6); + + // Test finishComposingText + ic.finishComposingText(); + assertTextAndSelectionAt("Can finish composition", ic, "foobar", 6); + + // Test setComposingRegion + ic.setComposingRegion(0, 3); + assertTextAndSelectionAt("Can set composing region", ic, "foobar", 6); + + ic.setComposingText("far", 1); + assertTextAndSelectionAt("Can set composing region text", ic, "farbar", 3); + + ic.setComposingRegion(1, 4); + assertTextAndSelectionAt("Can set existing composing region", ic, "farbar", 3); + + ic.setComposingText("rab", 3); + assertTextAndSelectionAt("Can set new composing region text", ic, "frabar", 6); + + // Test getTextBeforeCursor + fAssertEquals("Can retrieve text before cursor", "bar", ic.getTextBeforeCursor(3, 0)); + + // Test getTextAfterCursor + fAssertEquals("Can retrieve text after cursor", "", ic.getTextAfterCursor(3, 0)); + + ic.finishComposingText(); + assertTextAndSelectionAt("Can finish composition", ic, "frabar", 6); + + // Test sendKeyEvent + final KeyEvent shiftKey = new KeyEvent(KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SHIFT_LEFT); + final KeyEvent leftKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); + final KeyEvent tKey = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_T); + + ic.sendKeyEvent(shiftKey); + ic.sendKeyEvent(leftKey); + ic.sendKeyEvent(KeyEvent.changeAction(leftKey, KeyEvent.ACTION_UP)); + ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP)); + assertTextAndSelection("Can select using key event", ic, "frabar", 6, 5); + + ic.sendKeyEvent(tKey); + ic.sendKeyEvent(KeyEvent.changeAction(tKey, KeyEvent.ACTION_UP)); + assertTextAndSelectionAt("Can type using event", ic, "frabat", 6); + + ic.deleteSurroundingText(6, 0); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Bug 1133802, duplication when setting the same composing text more than once. + ic.setComposingText("foo", 1); + assertTextAndSelectionAt("Can set the composing text", ic, "foo", 3); + ic.setComposingText("foo", 1); + assertTextAndSelectionAt("Can set the same composing text", ic, "foo", 3); + ic.setComposingText("bar", 1); + assertTextAndSelectionAt("Can set different composing text", ic, "bar", 3); + ic.setComposingText("bar", 1); + assertTextAndSelectionAt("Can set the same composing text", ic, "bar", 3); + ic.setComposingText("bar", 1); + assertTextAndSelectionAt("Can set the same composing text again", ic, "bar", 3); + ic.finishComposingText(); + assertTextAndSelectionAt("Can finish composing text", ic, "bar", 3); + + ic.deleteSurroundingText(3, 0); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Bug 1209465, cannot enter ideographic space character by itself (U+3000). + ic.commitText("\u3000", 1); + assertTextAndSelectionAt("Can commit ideographic space", ic, "\u3000", 1); + + ic.deleteSurroundingText(1, 0); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Bug 1051556, exception due to committing text changes during flushing. + ic.setComposingText("bad", 1); + assertTextAndSelectionAt("Can set the composing text", ic, "bad", 3); + getJS().asyncCall("test_reflush_changes"); + // Wait for text change notifications to come in. + processGeckoEvents(); + assertTextAndSelectionAt("Can re-flush text changes", ic, "good", 4); + ic.setComposingText("done", 1); + assertTextAndSelectionAt("Can update composition after re-flushing", ic, "gooddone", 8); + ic.finishComposingText(); + assertTextAndSelectionAt("Can finish composing text", ic, "gooddone", 8); + + ic.deleteSurroundingText(8, 0); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Bug 1241558 - wrong selection due to ignoring selection notification. + ic.setComposingText("foobar", 1); + assertTextAndSelectionAt("Can set the composing text", ic, "foobar", 6); + getJS().asyncCall("test_set_selection"); + // Wait for text change notifications to come in. + processGeckoEvents(); + assertTextAndSelectionAt("Can select after committing", ic, "foobar", 3); + ic.setComposingText("barfoo", 1); + assertTextAndSelectionAt("Can compose after selecting", ic, "barfoo", 6); + ic.beginBatchEdit(); + ic.setSelection(3, 3); + ic.finishComposingText(); + ic.deleteSurroundingText(1, 1); + ic.endBatchEdit(); + assertTextAndSelectionAt("Can delete after committing", ic, "baoo", 2); + + ic.deleteSurroundingText(2, 2); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Bug 1275371 - shift+backspace should not forward delete on Android. + final KeyEvent delKey = new KeyEvent(KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_DEL); + + ic.beginBatchEdit(); + ic.commitText("foo", 1); + ic.setSelection(1, 1); + ic.endBatchEdit(); + assertTextAndSelectionAt("Can commit text", ic, "foo", 1); + + ic.sendKeyEvent(shiftKey); + ic.sendKeyEvent(delKey); + ic.sendKeyEvent(KeyEvent.changeAction(delKey, KeyEvent.ACTION_UP)); + assertTextAndSelectionAt("Can backspace with shift+backspace", ic, "oo", 0); + + ic.sendKeyEvent(delKey); + ic.sendKeyEvent(KeyEvent.changeAction(delKey, KeyEvent.ACTION_UP)); + ic.sendKeyEvent(KeyEvent.changeAction(shiftKey, KeyEvent.ACTION_UP)); + assertTextAndSelectionAt("Cannot forward delete with shift+backspace", ic, "oo", 0); + + ic.deleteSurroundingText(0, 2); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Bug 1123514 - exception due to incorrect text replacement offsets. + getJS().syncCall("test_bug1123514"); + // Gecko will change text to 'abc' when we input 'b', potentially causing + // incorrect calculation of text replacement offsets. + ic.commitText("b", 1); + // We don't assert text here because this test only works for input/textarea, + // so an assertion would fail for contentEditable/designMode. + processGeckoEvents(); + processInputConnectionEvents(); + + ic.deleteSurroundingText(2, 1); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Make sure we don't leave behind stale events for the following test. + processGeckoEvents(); + processInputConnectionEvents(); + } + } + + /** + * ResettingInputConnectionTest performs tests on the resetting input in + * robocop_input.html. Any test that uses the normal input should be put in + * BasicInputConnectionTest. + */ + private class ResettingInputConnectionTest extends InputConnectionTest { + @Override + public void test(final InputConnection ic, EditorInfo info) { + waitFor("focus change", new Condition() { + @Override + public boolean isSatisfied() { + return "".equals(getText(ic)); + } + }); + + // Bug 1199658, duplication when page has JS that resets input field value. + + ic.commitText("foo", 1); + assertTextAndSelectionAt("Can commit text (resetting)", ic, "foo", 3); + + ic.setComposingRegion(0, 3); + // The bug appears after composition update events are processed. We only + // issue these events after some back-and-forth calls between the Gecko thread + // and the input connection thread. Therefore, to ensure these events are + // issued and to ensure the bug appears, we have to process all Gecko events, + // then all input connection events, and finally all Gecko events again. + processGeckoEvents(); + processInputConnectionEvents(); + processGeckoEvents(); + assertTextAndSelectionAt("Can set composing region (resetting)", ic, "foo", 3); + + ic.setComposingText("foobar", 1); + processGeckoEvents(); + processInputConnectionEvents(); + processGeckoEvents(); + assertTextAndSelectionAt("Can change composing text (resetting)", ic, "foobar", 6); + + ic.setComposingText("baz", 1); + processGeckoEvents(); + processInputConnectionEvents(); + processGeckoEvents(); + assertTextAndSelectionAt("Can reset composing text (resetting)", ic, "baz", 3); + + ic.finishComposingText(); + assertTextAndSelectionAt("Can finish composing text (resetting)", ic, "baz", 3); + + ic.deleteSurroundingText(3, 0); + assertTextAndSelectionAt("Can clear text", ic, "", 0); + + // Make sure we don't leave behind stale events for the following test. + processGeckoEvents(); + processInputConnectionEvents(); + } + } + + /** + * HidingInputConnectionTest performs tests on the hiding input in + * robocop_input.html. Any test that uses the normal input should be put in + * BasicInputConnectionTest. + */ + private class HidingInputConnectionTest extends InputConnectionTest { + @Override + public void test(final InputConnection ic, EditorInfo info) { + waitFor("focus change", new Condition() { + @Override + public boolean isSatisfied() { + return "".equals(getText(ic)); + } + }); + + // Bug 1254629, crash when hiding input during input. + ic.commitText("foo", 1); + assertTextAndSelectionAt("Can commit text (hiding)", ic, "foo", 3); + + ic.commitText("!", 1); + // The '!' key causes the input to hide in robocop_input.html, + // and there won't be a text/selection update as a result. + assertTextAndSelectionAt("Can handle hiding input", ic, "foo", 3); + + // Make sure we don't leave behind stale events for the following test. + processGeckoEvents(); + processInputConnectionEvents(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java new file mode 100644 index 000000000..c12ccef98 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testInputUrlBar.java @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Element; +import org.mozilla.gecko.R; + +import android.widget.EditText; + +/** + * Basic test of text editing within the editing mode. + * - Enter some text, move the cursor around, and modifying some text. + * - Check that all edit entry text is selected after switching about:home tabs. + */ +public final class testInputUrlBar extends BaseTest { + private Element mUrlBarEditElement; + private EditText mUrlBarEditView; + + public void testInputUrlBar() { + blockForGeckoReady(); + + startEditingMode(); + assertUrlBarText(""); + + // Avoid any auto domain completion by using a prefix that matches + // nothing, including about: pages + mActions.sendKeys("zy"); + assertUrlBarText("zy"); + + mActions.sendKeys("cd"); + assertUrlBarText("zycd"); + + mActions.sendSpecialKey(Actions.SpecialKey.LEFT); + mActions.sendSpecialKey(Actions.SpecialKey.LEFT); + + // Inserting "" should not do anything. + mActions.sendKeys(""); + assertUrlBarText("zycd"); + + mActions.sendKeys("ef"); + assertUrlBarText("zyefcd"); + + mActions.sendSpecialKey(Actions.SpecialKey.RIGHT); + mActions.sendKeys("gh"); + assertUrlBarText("zyefcghd"); + + final EditText editText = mUrlBarEditView; + runOnUiThreadSync(new Runnable() { + @Override + public void run() { + // Select "ef" + editText.setSelection(2); + } + }); + mActions.sendKeys("op"); + assertUrlBarText("zyopefcghd"); + + runOnUiThreadSync(new Runnable() { + @Override + public void run() { + // Select "cg" + editText.setSelection(6, 8); + } + }); + mActions.sendKeys("qr"); + assertUrlBarText("zyopefqrhd"); + + runOnUiThreadSync(new Runnable() { + @Override + public void run() { + // Select "op" + editText.setSelection(4,2); + } + }); + mActions.sendKeys("st"); + assertUrlBarText("zystefqrhd"); + + runOnUiThreadSync(new Runnable() { + @Override + public void run() { + editText.selectAll(); + } + }); + mActions.sendKeys("uv"); + assertUrlBarText("uv"); + + // Dismiss the VKB + mSolo.goBack(); + + // Dismiss editing mode + mSolo.goBack(); + + waitForText(mStringHelper.TITLE_PLACE_HOLDER); + + // URL bar should have forgotten about "uv" text. + startEditingMode(); + assertUrlBarText(""); + + int width = mDriver.getGeckoWidth() / 2; + int y = mDriver.getGeckoHeight() / 2; + + // Slide to the right, force URL bar entry to lose input focus + mActions.drag(width, 0, y, y); + + // Select text and replace the content + mSolo.clickOnView(mUrlBarEditView); + mActions.sendKeys("yz"); + + String yz = getUrlBarText(); + mAsserter.ok("yz".equals(yz), "Is the URL bar text \"yz\"?", yz); + } + + private void startEditingMode() { + focusUrlBar(); + + mUrlBarEditElement = mDriver.findElement(getActivity(), R.id.url_edit_text); + final int id = mUrlBarEditElement.getId(); + mUrlBarEditView = (EditText) getActivity().findViewById(id); + } + + private String getUrlBarText() { + final String elementText = mUrlBarEditElement.getText(); + final String editText = mUrlBarEditView.getText().toString(); + mAsserter.is(editText, elementText, "Does URL bar editText == elementText?"); + + return editText; + } + + private void assertUrlBarText(String expectedText) { + String actualText = getUrlBarText(); + mAsserter.is(actualText, expectedText, "Does URL bar actualText == expectedText?"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java new file mode 100644 index 000000000..9310599d3 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJarReader.java @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.InputStream; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.util.GeckoJarReader; + +import android.content.Context; + +/** + * A basic jar reader test. Tests reading a png from fennec's apk, as well + * as loading some invalid jar urls. + */ +public class testJarReader extends BaseTest { + public void testJarReader() { + // Invalid characters are escaped. + final String s = GeckoJarReader.computeJarURI("some[1].apk", "something/else"); + mAsserter.ok(!s.contains("["), "Illegal characters are escaped away.", null); + mAsserter.ok(!s.toLowerCase().contains("%2f"), "Path characters aren't escaped.", null); + + final Context context = getInstrumentation().getTargetContext().getApplicationContext(); + String appPath = getActivity().getApplication().getPackageResourcePath(); + mAsserter.isnot(appPath, null, "getPackageResourcePath is non-null"); + + // Test reading a file from a jar url that looks correct. + String url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME; + InputStream stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png"); + mAsserter.isnot(stream, null, "JarReader returned non-null for valid file in valid jar"); + + // Test looking for an non-existent file in a jar. + url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png"); + mAsserter.is(stream, null, "JarReader returned null for non-existent file in valid jar"); + + // Test looking for a file that doesn't exist in the APK. + url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png"); + mAsserter.is(stream, null, "JarReader returned null for valid file in invalid jar file"); + + // Test looking for a file that doesn't exist in the APK. + // Bug 1174922, prefixed string / length error. + url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME + "BAD"; + stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png"); + mAsserter.is(stream, null, "JarReader returned null for valid file in other invalid jar file"); + + // Test looking for an jar with an invalid url. + url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png"); + mAsserter.is(stream, null, "JarReader returned null for bad jar url"); + + // Test looking for a file that doesn't exist on disk. + url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME; + stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png"); + mAsserter.is(stream, null, "JarReader returned null for a non-existent APK"); + + // This test completes very quickly. If it completes too soon, the + // minidumps directory may not be created before the process is + // taken down, causing bug 722166. + blockForGeckoReady(); + } + + private String getData(InputStream stream) { + return new java.util.Scanner(stream).useDelimiter("\\A").next(); + } + +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java new file mode 100644 index 000000000..724b6b4ab --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testJavascriptBridge.java @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Tests the proper operation of JavascriptBridge and JavaBridge, + * which are used by tests for communication between Java and JS. + */ +public class testJavascriptBridge extends JavascriptBridgeTest { + + private static final String TEST_JS = "testJavascriptBridge.js"; + + private boolean syncCallReceived; + + public void testJavascriptBridge() { + blockForReadyAndLoadJS(TEST_JS); + getJS().syncCall("check_js_int_arg", 1); + } + + public void checkJavaIntArg(final int int2) { + // Async call from JS + fAssertEquals("Integer argument matches", 2, int2); + getJS().syncCall("check_js_double_arg", 3.0D); + } + + public void checkJavaDoubleArg(final double double4) { + // Async call from JS + fAssertEquals("Double argument matches", 4.0, double4); + getJS().syncCall("check_js_boolean_arg", false); + } + + public void checkJavaBooleanArg(final boolean booltrue) { + // Async call from JS + fAssertEquals("Boolean argument matches", true, booltrue); + getJS().syncCall("check_js_string_arg", "foo"); + } + + public void checkJavaStringArg(final String stringbar) throws JSONException { + // Async call from JS + fAssertEquals("String argument matches", "bar", stringbar); + final JSONObject obj = new JSONObject(); + obj.put("caller", "java"); + getJS().syncCall("check_js_object_arg", (JSONObject) obj); + } + + public void checkJavaObjectArg(final JSONObject obj) throws JSONException { + // Async call from JS + fAssertEquals("Object argument matches", "js", obj.getString("caller")); + getJS().syncCall("check_js_sync_call"); + } + + public void doJSSyncCall() { + // Sync call from JS + syncCallReceived = true; + getJS().asyncCall("respond_to_js_sync_call"); + } + + public void checkJSSyncCallReceived() { + fAssertTrue("Received sync call before end of test", syncCallReceived); + // End of test + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java new file mode 100644 index 000000000..556ed0e07 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLinkContextMenu.java @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +public class testLinkContextMenu extends ContentContextMenuTest { + + // Test website strings + private static String LINK_PAGE_URL; + private static String BLANK_PAGE_URL; + private static final String LINK_PAGE_TITLE = "Big Link"; + + public void testLinkContextMenu() { + final String linkMenuItems [] = mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB; + + blockForGeckoReady(); + + LINK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL); + BLANK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + loadUrlAndWait(LINK_PAGE_URL); + waitForText(LINK_PAGE_TITLE); + + verifyContextMenuItems(linkMenuItems); // Verify context menu items are correct + openTabFromContextMenu(linkMenuItems[0],2); // Test the "Open in New Tab" option - expecting 2 tabs: the original and the new one + openTabFromContextMenu(linkMenuItems[1],2); // Test the "Open in Private Tab" option - expecting only 2 tabs in normal mode + verifyCopyOption(linkMenuItems[2], BLANK_PAGE_URL); // Test the "Copy Link" option + verifyShareOption(linkMenuItems[3], LINK_PAGE_TITLE); // Test the "Share Link" option + verifyBookmarkLinkOption(linkMenuItems[4], BLANK_PAGE_URL); // Test the "Bookmark Link" option + } + + @Override + public void tearDown() throws Exception { + mDatabaseHelper.deleteBookmark(BLANK_PAGE_URL); + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java new file mode 100644 index 000000000..e62bd7899 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoad.java @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +/** + * A basic page load test. + * - loads a page + * - verifies it rendered properly + * - verifies the displayed url is correct + */ +public class testLoad extends PixelTest { + public void testLoad() { + String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL); + + blockForGeckoReady(); + + loadAndVerifyBoxes(url); + + verifyUrl(url); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java new file mode 100644 index 000000000..10dde28cd --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testLoginsProvider.java @@ -0,0 +1,387 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.DeletedLogins; +import org.mozilla.gecko.db.BrowserContract.Logins; +import org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts; +import org.mozilla.gecko.db.LoginsProvider; + +import java.util.concurrent.Callable; + +import static org.mozilla.gecko.db.BrowserContract.CommonColumns._ID; +import static org.mozilla.gecko.db.BrowserContract.DeletedLogins.TABLE_DELETED_LOGINS; +import static org.mozilla.gecko.db.BrowserContract.Logins.TABLE_LOGINS; +import static org.mozilla.gecko.db.BrowserContract.LoginsDisabledHosts.TABLE_DISABLED_HOSTS; + +public class testLoginsProvider extends ContentProviderTest { + + private static final String DB_NAME = "browser.db"; + + private final TestCase[] TESTS_TO_RUN = { + new InsertLoginsTest(), + new UpdateLoginsTest(), + new DeleteLoginsTest(), + new InsertDeletedLoginsTest(), + new InsertDeletedLoginsFailureTest(), + new DisabledHostsInsertTest(), + new DisabledHostsInsertFailureTest(), + new InsertLoginsWithDefaultValuesTest(), + new InsertLoginsWithDuplicateGuidFailureTest(), + new DeleteLoginsByNonExistentGuidTest(), + }; + + /** + * Factory function that makes new LoginsProvider instances. + * <p> + * We want a fresh provider each test, so this should be invoked in + * <code>setUp</code> before each individual test. + */ + private static final Callable<ContentProvider> sProviderFactory = new Callable<ContentProvider>() { + @Override + public ContentProvider call() { + return new LoginsProvider(); + } + }; + + @Override + public void setUp() throws Exception { + super.setUp(sProviderFactory, BrowserContract.LOGINS_AUTHORITY, DB_NAME); + for (TestCase test: TESTS_TO_RUN) { + mTests.add(test); + } + } + + public void testLoginProviderTests() throws Exception { + for (Runnable test : mTests) { + final String testName = test.getClass().getSimpleName(); + setTestName(testName); + ensureEmptyDatabase(); + mAsserter.dumpLog("testLoginsProvider: Database empty - Starting " + testName + "."); + test.run(); + } + } + + /** + * Wipe DB. + */ + private void ensureEmptyDatabase() { + getWritableDatabase(Logins.CONTENT_URI).delete(TABLE_LOGINS, null, null); + getWritableDatabase(DeletedLogins.CONTENT_URI).delete(TABLE_DELETED_LOGINS, null, null); + getWritableDatabase(LoginsDisabledHosts.CONTENT_URI).delete(TABLE_DISABLED_HOSTS, null, null); + } + + private SQLiteDatabase getWritableDatabase(Uri uri) { + Uri testUri = appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1"); + DelegatingTestContentProvider delegateProvider = (DelegatingTestContentProvider) mProvider; + LoginsProvider loginsProvider = (LoginsProvider) delegateProvider.getTargetProvider(); + return loginsProvider.getWritableDatabaseForTesting(testUri); + } + + /** + * LoginsProvider insert logins test. + */ + private class InsertLoginsTest extends TestCase { + @Override + public void test() throws Exception { + ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com", + "http://www.example.com", "username1", "password1", "username1", "password1", "guid1"); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues)); + verifyLoginExists(contentValues, id); + Cursor cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid1" }, null); + verifyRowMatches(contentValues, cursor, "logins found"); + + // Empty ("") encrypted username and password are valid. + contentValues = createLogin("http://www.example.com", "http://www.example.com", + "http://www.example.com", "username1", "password1", "", "", "guid2"); + id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues)); + verifyLoginExists(contentValues, id); + cursor = mProvider.query(Logins.CONTENT_URI, null, Logins.GUID + " = ?", new String[] { "guid2" }, null); + verifyRowMatches(contentValues, cursor, "logins found"); + } + } + + /** + * LoginsProvider updates logins test. + */ + private class UpdateLoginsTest extends TestCase { + @Override + public void test() throws Exception { + final String guid1 = "guid1"; + ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com", + "http://www.example.com", "username1", "password1", "username1", "password1", guid1); + long timeBeforeCreated = System.currentTimeMillis(); + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues)); + long timeAfterCreated = System.currentTimeMillis(); + verifyLoginExists(contentValues, id); + + Cursor cursor = getLoginById(id); + try { + mAsserter.ok(cursor.moveToFirst(), "cursor is not empty", ""); + verifyBounded(timeBeforeCreated, cursor.getLong(cursor.getColumnIndexOrThrow(Logins.TIME_CREATED)), timeAfterCreated); + } finally { + cursor.close(); + } + + contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username2"); + contentValues.put(Logins.ENCRYPTED_PASSWORD, "password2"); + + Uri updateUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); + int numUpdated = mProvider.update(updateUri, contentValues, null, null); + mAsserter.is(1, numUpdated, "Correct number updated"); + verifyLoginExists(contentValues, id); + + contentValues.put(BrowserContract.Logins.ENCRYPTED_USERNAME, "username1"); + contentValues.put(Logins.ENCRYPTED_PASSWORD, "password1"); + + updateUri = Logins.CONTENT_URI; + numUpdated = mProvider.update(updateUri, contentValues, Logins.GUID + " = ?", new String[]{guid1}); + mAsserter.is(1, numUpdated, "Correct number updated"); + verifyLoginExists(contentValues, id); + } + } + + /** + * LoginsProvider deletion logins test. + * - inserts a new logins + * - deletes the logins and verify deleted-logins table has entry for deleted guid. + */ + private class DeleteLoginsTest extends TestCase { + @Override + public void test() throws Exception { + final String guid1 = "guid1"; + ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com", + "http://www.example.com", "username1", "password1", "username1", "password1", guid1); + long id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues)); + verifyLoginExists(contentValues, id); + + Uri deletedUri = Logins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); + int numDeleted = mProvider.delete(deletedUri, null, null); + mAsserter.is(1, numDeleted, "Correct number deleted"); + verifyNoRowExists(Logins.CONTENT_URI, "No login entry found"); + + contentValues = new ContentValues(); + contentValues.put(DeletedLogins.GUID, guid1); + Cursor cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, null, null, null); + verifyRowMatches(contentValues, cursor, "deleted-login found"); + cursor = mProvider.query(DeletedLogins.CONTENT_URI, null, DeletedLogins.GUID + " = ?", new String[] { guid1 }, null); + verifyRowMatches(contentValues, cursor, "deleted-login found"); + } + } + + /** + * LoginsProvider re-insert logins test. + * - inserts a row into deleted-logins + * - insert the same login (matching guid) and verify deleted-logins table is empty. + */ + private class InsertDeletedLoginsTest extends TestCase { + @Override + public void test() throws Exception { + ContentValues contentValues = new ContentValues(); + contentValues.put(DeletedLogins.GUID, "guid1"); + long id = ContentUris.parseId(mProvider.insert(DeletedLogins.CONTENT_URI, contentValues)); + final Uri insertedUri = DeletedLogins.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); + Cursor cursor = mProvider.query(insertedUri, null, null, null, null); + verifyRowMatches(contentValues, cursor, "deleted-login found"); + verifyNoRowExists(BrowserContract.Logins.CONTENT_URI, "No login entry found"); + + contentValues = createLogin("http://www.example.com", "http://www.example.com", + "http://www.example.com", "username1", "password1", "username1", "password1", "guid1"); + id = ContentUris.parseId(mProvider.insert(Logins.CONTENT_URI, contentValues)); + verifyLoginExists(contentValues, id); + verifyNoRowExists(DeletedLogins.CONTENT_URI, "No deleted-login entry found"); + } + } + + /** + * LoginsProvider insert Deleted logins test. + * - inserts a row into deleted-login without GUID. + */ + private class InsertDeletedLoginsFailureTest extends TestCase { + @Override + public void test() throws Exception { + ContentValues contentValues = new ContentValues(); + try { + mProvider.insert(DeletedLogins.CONTENT_URI, contentValues); + fail("Failed to throw IllegalArgumentException while missing GUID"); + } catch (Exception e) { + mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid GUID"); + } + } + } + + /** + * LoginsProvider disabled host test. + * - inserts a disabled-host + * - delete the inserted disabled-host and verify disabled-hosts table is empty. + */ + private class DisabledHostsInsertTest extends TestCase { + @Override + public void test() throws Exception { + final String hostname = "localhost"; + final ContentValues contentValues = new ContentValues(); + contentValues.put(LoginsDisabledHosts.HOSTNAME, hostname); + mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues); + final Uri insertedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build(); + final Cursor cursor = mProvider.query(insertedUri, null, null, null, null); + verifyRowMatches(contentValues, cursor, "disabled-hosts found"); + + final Uri deletedUri = LoginsDisabledHosts.CONTENT_URI.buildUpon().appendPath("hostname").appendPath(hostname).build(); + final int numDeleted = mProvider.delete(deletedUri, null, null); + mAsserter.is(1, numDeleted, "Correct number deleted"); + verifyNoRowExists(LoginsDisabledHosts.CONTENT_URI, "No disabled-hosts entry found"); + } + } + + /** + * LoginsProvider disabled host insert failure testcase. + * - inserts a disabled-host without providing hostname + */ + private class DisabledHostsInsertFailureTest extends TestCase { + @Override + public void test() throws Exception { + final String hostname = "localhost"; + final ContentValues contentValues = new ContentValues(); + try { + mProvider.insert(LoginsDisabledHosts.CONTENT_URI, contentValues); + fail("Failed to throw IllegalArgumentException while missing hostname"); + } catch (Exception e) { + mAsserter.is(e.getClass(), IllegalArgumentException.class, "IllegalArgumentException thrown for invalid hostname"); + } + } + } + + /** + * LoginsProvider login insertion with default values test. + * - insert a login missing GUID, FORM_SUBMIT_URL, HTTP_REALM and verify default values are set. + */ + private class InsertLoginsWithDefaultValuesTest extends TestCase { + @Override + protected void test() throws Exception { + ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com", + "http://www.example.com", "username1", "password1", "username1", "password1", null); + // Remove GUID, HTTP_REALM, FORM_SUBMIT_URL from content values + contentValues.remove(Logins.GUID); + contentValues.remove(Logins.FORM_SUBMIT_URL); + contentValues.remove(Logins.HTTP_REALM); + + long id = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues)); + Cursor cursor = getLoginById(id); + assertNotNull(cursor); + cursor.moveToFirst(); + + mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.GUID)), null, "GUID is not null"); + mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.HTTP_REALM)), null, "HTTP_REALM is not null"); + mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.FORM_SUBMIT_URL)), null, "FORM_SUBMIT_URL is not null"); + mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_LAST_USED)), null, "TIME_LAST_USED is not null"); + mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_CREATED)), null, "TIME_CREATED is not null"); + mAsserter.isnot(cursor.getString(cursor.getColumnIndex(Logins.TIME_PASSWORD_CHANGED)), null, "TIME_PASSWORD_CHANGED is not null"); + mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.ENC_TYPE)), "0", "ENC_TYPE is 0"); + mAsserter.is(cursor.getString(cursor.getColumnIndex(Logins.TIMES_USED)), "0", "TIMES_USED is 0"); + + // Verify other values. + verifyRowMatches(contentValues, cursor, "Updated login found"); + } + } + + /** + * LoginsProvider login insertion with duplicate GUID test. + * - insert two different logins with same GUID and verify that only one login exists. + */ + private class InsertLoginsWithDuplicateGuidFailureTest extends TestCase { + @Override + protected void test() throws Exception { + final String guid = "guid1"; + ContentValues contentValues = createLogin("http://www.example.com", "http://www.example.com", + "http://www.example.com", "username1", "password1", "username1", "password1", guid); + long id1 = ContentUris.parseId(mProvider.insert(BrowserContract.Logins.CONTENT_URI, contentValues)); + verifyLoginExists(contentValues, id1); + + // Insert another login with duplicate GUID. + contentValues = createLogin("http://www.example2.com", "http://www.example2.com", + "http://www.example2.com", "username2", "password2", "username2", "password2", guid); + Uri insertUri = mProvider.insert(Logins.CONTENT_URI, contentValues); + mAsserter.is(insertUri, null, "Duplicate Guid insertion id1"); + + // Verify login with id1 still exists. + verifyLoginExists(contentValues, id1); + } + } + + /** + * LoginsProvider deletion by non-existent GUID test. + * - delete a login with random GUID and verify that no entry was deleted. + */ + private class DeleteLoginsByNonExistentGuidTest extends TestCase { + @Override + protected void test() throws Exception { + Uri deletedUri = Logins.CONTENT_URI; + int numDeleted = mProvider.delete(deletedUri, Logins.GUID + "= ?", new String[] { "guid1" }); + mAsserter.is(0, numDeleted, "Correct number deleted"); + } + } + + private void verifyBounded(long left, long middle, long right) { + mAsserter.ok(left <= middle, "Left <= middle", left + " <= " + middle); + mAsserter.ok(middle <= right, "Middle <= right", middle + " <= " + right); + } + + private Cursor getById(Uri uri, long id, String[] projection) { + return mProvider.query(uri, projection, + _ID + " = ?", + new String[] { String.valueOf(id) }, + null); + } + + private Cursor getLoginById(long id) { + return getById(Logins.CONTENT_URI, id, null); + } + + private void verifyLoginExists(ContentValues contentValues, long id) { + Cursor cursor = getLoginById(id); + verifyRowMatches(contentValues, cursor, "Updated login found"); + } + + private void verifyRowMatches(ContentValues contentValues, Cursor cursor, String name) { + try { + mAsserter.ok(cursor.moveToFirst(), name, "cursor is not empty"); + CursorMatches(cursor, contentValues); + } finally { + cursor.close(); + } + } + + private void verifyNoRowExists(Uri contentUri, String name) { + Cursor cursor = mProvider.query(contentUri, null, null, null, null); + try { + mAsserter.is(0, cursor.getCount(), name); + } finally { + cursor.close(); + } + } + + private ContentValues createLogin(String hostname, String httpRealm, String formSubmitUrl, + String usernameField, String passwordField, String encryptedUsername, + String encryptedPassword, String guid) { + final ContentValues values = new ContentValues(); + values.put(Logins.HOSTNAME, hostname); + values.put(Logins.HTTP_REALM, httpRealm); + values.put(Logins.FORM_SUBMIT_URL, formSubmitUrl); + values.put(Logins.USERNAME_FIELD, usernameField); + values.put(Logins.PASSWORD_FIELD, passwordField); + values.put(Logins.ENCRYPTED_USERNAME, encryptedUsername); + values.put(Logins.ENCRYPTED_PASSWORD, encryptedPassword); + values.put(Logins.GUID, guid); + return values; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java new file mode 100644 index 000000000..af674f441 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testMailToContextMenu.java @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +public class testMailToContextMenu extends ContentContextMenuTest { + + // Test website strings + private static String MAILTO_PAGE_URL; + private static final String mailtoMenuItems [] = {"Copy Email Address", "Share Email Address"}; + + public void testMailToContextMenu() { + final String MAILTO_PAGE_TITLE = mStringHelper.ROBOCOP_BIG_MAILTO_TITLE; + + blockForGeckoReady(); + + MAILTO_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_MAILTO_URL); + loadUrlAndWait(MAILTO_PAGE_URL); + waitForText(MAILTO_PAGE_TITLE); + + verifyContextMenuItems(mailtoMenuItems); + verifyCopyOption(mailtoMenuItems[0], "foo.bar@example.com"); // Test the "Copy Email Address" option + verifyShareOption(mailtoMenuItems[1], MAILTO_PAGE_TITLE); // Test the "Share Email Address" option + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java new file mode 100644 index 000000000..2ae2bb532 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNativeCrypto.java @@ -0,0 +1,288 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertArrayEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.mozilla.gecko.background.nativecode.NativeCrypto; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.tests.helpers.GeckoHelper; + +import android.os.SystemClock; + +/** + * Tests the Java wrapper over native implementations of crypto code. Test vectors from: + * * PBKDF2SHA256: + * - <https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors> + - <https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c> + * SHA-1: + - <http://oauth.googlecode.com/svn/code/c/liboauth/src/sha1.c> + */ +public class testNativeCrypto extends UITest { + private final static String LOGTAG = "testNativeCrypto"; + + /** + * Robocop supports only a single test function per test class. Therefore, we + * have a single top-level test function that dispatches to sub-tests, + * accepting that we might fail part way through the cycle. Proper JUnit 3 + * testing can't land soon enough! + * + * @throws Exception + */ + public void test() throws Exception { + // This test could complete very quickly. If it completes too soon, the + // minidumps directory may not be created before the process is + // taken down, causing bug 722166. But we can't run the test and then block + // for Gecko:Ready, since it may have arrived before we block. So we wait. + // Again, JUnit 3 can't land soon enough! + GeckoHelper.blockForReady(); + + _testPBKDF2SHA256A(); + _testPBKDF2SHA256B(); + _testPBKDF2SHA256C(); + _testPBKDF2SHA256scryptA(); + _testPBKDF2SHA256scryptB(); + _testPBKDF2SHA256InvalidLenArg(); + + _testSHA1(); + _testSHA1AgainstMessageDigest(); + + _testSHA256(); + _testSHA256MultiPart(); + _testSHA256AgainstMessageDigest(); + _testSHA256WithMultipleUpdatesFromStream(); + } + + public void _testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException { + final String p = "password"; + final String s = "salt"; + final int dkLen = 32; + + checkPBKDF2SHA256(p, s, 1, dkLen, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"); + checkPBKDF2SHA256(p, s, 4096, dkLen, "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a"); + } + + public void _testPBKDF2SHA256B() throws UnsupportedEncodingException, GeneralSecurityException { + final String p = "passwordPASSWORDpassword"; + final String s = "saltSALTsaltSALTsaltSALTsaltSALTsalt"; + final int dkLen = 40; + + checkPBKDF2SHA256(p, s, 4096, dkLen, "348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9"); + } + + public void _testPBKDF2SHA256scryptA() throws UnsupportedEncodingException, GeneralSecurityException { + final String p = "passwd"; + final String s = "salt"; + final int dkLen = 64; + + checkPBKDF2SHA256(p, s, 1, dkLen, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783"); + } + + public void _testPBKDF2SHA256scryptB() throws UnsupportedEncodingException, GeneralSecurityException { + final String p = "Password"; + final String s = "NaCl"; + final int dkLen = 64; + + checkPBKDF2SHA256(p, s, 80000, dkLen, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d"); + } + + public void _testPBKDF2SHA256C() throws UnsupportedEncodingException, GeneralSecurityException { + final String p = "pass\0word"; + final String s = "sa\0lt"; + final int dkLen = 16; + + checkPBKDF2SHA256(p, s, 4096, dkLen, "89b69d0516f829893c696226650a8687"); + } + + public void _testPBKDF2SHA256InvalidLenArg() throws UnsupportedEncodingException, GeneralSecurityException { + final String p = "password"; + final String s = "salt"; + final int c = 1; + final int dkLen = -1; // Should always be positive. + + try { + final byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen); + fFail("Expected sha256 to throw with negative dkLen argument."); + } catch (IllegalArgumentException e) { } // Expected. + } + + private void _testSHA1() throws UnsupportedEncodingException { + final String[] inputs = new String[] { + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "" // To be filled in below. + }; + final String baseStr = "01234567"; + final int repetitions = 80; + final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions); + for (int i = 0; i < 80; ++i) { + builder.append(baseStr); + } + inputs[2] = builder.toString(); + + final String[] expecteds = new String[] { + "a9993e364706816aba3e25717850c26c9cd0d89d", + "84983e441c3bd26ebaae4aa1f95129e5e54670f1", + "dea356a2cddd90c7a7ecedc5ebb563934f460452" + }; + + for (int i = 0; i < inputs.length; ++i) { + final byte[] input = inputs[i].getBytes("US-ASCII"); + final String expected = expecteds[i]; + + final byte[] actual = NativeCrypto.sha1(input); + fAssertNotNull("Hashed value is non-null", actual); + assertExpectedBytes(expected, actual); + } + } + + /** + * Test to ensure the output of our SHA1 algo is the same as MessageDigest's. This is important + * because we intend to replace MessageDigest in FHR with this SHA-1 algo (bug 959652). + */ + private void _testSHA1AgainstMessageDigest() throws UnsupportedEncodingException, + NoSuchAlgorithmException { + final String[] inputs = { + "password", + "saranghae", + "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!" + }; + + final MessageDigest digest = MessageDigest.getInstance("SHA-1"); + for (final String input : inputs) { + final byte[] inputBytes = input.getBytes("US-ASCII"); + + final byte[] mdBytes = digest.digest(inputBytes); + final byte[] ourBytes = NativeCrypto.sha1(inputBytes); + fAssertArrayEquals("MessageDigest hash is the same as NativeCrypto SHA-1 hash", mdBytes, ourBytes); + } + } + + private void _testSHA256() throws UnsupportedEncodingException { + final String[] inputs = new String[] { + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "" // To be filled in below. + }; + final String baseStr = "01234567"; + final int repetitions = 80; + final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions); + for (int i = 0; i < repetitions; ++i) { + builder.append(baseStr); + } + inputs[2] = builder.toString(); + + final String[] expecteds = new String[] { + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", + "594847328451bdfa85056225462cc1d867d877fb388df0ce35f25ab5562bfbb5" + }; + + for (int i = 0; i < inputs.length; ++i) { + final byte[] input = inputs[i].getBytes("US-ASCII"); + final String expected = expecteds[i]; + + final byte[] ctx = NativeCrypto.sha256init(); + NativeCrypto.sha256update(ctx, input, input.length); + final byte[] actual = NativeCrypto.sha256finalize(ctx); + fAssertNotNull("Hashed value is non-null", actual); + assertExpectedBytes(expected, actual); + } + } + + private void _testSHA256MultiPart() throws UnsupportedEncodingException { + final String input = "01234567"; + final int repetitions = 80; + final String expected = "594847328451bdfa85056225462cc1d867d877fb388df0ce35f25ab5562bfbb5"; + + final byte[] inputBytes = input.getBytes("US-ASCII"); + final byte[] ctx = NativeCrypto.sha256init(); + for (int i = 0; i < repetitions; ++i) { + NativeCrypto.sha256update(ctx, inputBytes, inputBytes.length); + } + final byte[] actual = NativeCrypto.sha256finalize(ctx); + fAssertNotNull("Hashed value is non-null", actual); + assertExpectedBytes(expected, actual); + } + + private void _testSHA256AgainstMessageDigest() throws UnsupportedEncodingException, + NoSuchAlgorithmException { + final String[] inputs = { + "password", + "saranghae", + "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!" + }; + + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + for (final String input : inputs) { + final byte[] inputBytes = input.getBytes("US-ASCII"); + + final byte[] mdBytes = digest.digest(inputBytes); + + final byte[] ctx = NativeCrypto.sha256init(); + NativeCrypto.sha256update(ctx, inputBytes, inputBytes.length); + final byte[] ourBytes = NativeCrypto.sha256finalize(ctx); + fAssertArrayEquals("MessageDigest hash is the same as NativeCrypto SHA-256 hash", mdBytes, ourBytes); + } + } + + private void _testSHA256WithMultipleUpdatesFromStream() throws UnsupportedEncodingException { + final String input = "HelloWorldThisIsASuperLongStringThatIsReadAsAStreamOfBytes"; + final ByteArrayInputStream stream = new ByteArrayInputStream(input.getBytes("UTF-8")); + final String expected = "8b5cb76b80f7eb6fb83ee138bfd31e2922e71dd245daa21a8d9876e8dee9eef5"; + + byte[] buffer = new byte[10]; + final byte[] ctx = NativeCrypto.sha256init(); + int c; + + try { + while ((c = stream.read(buffer)) != -1) { + NativeCrypto.sha256update(ctx, buffer, c); + } + final byte[] actual = NativeCrypto.sha256finalize(ctx); + fAssertNotNull("Hashed value is non-null", actual); + assertExpectedBytes(expected, actual); + } catch (IOException e) { + fFail("IOException while reading stream"); + } + } + + private void checkPBKDF2SHA256(String p, String s, int c, int dkLen, final String expectedStr) + throws GeneralSecurityException, UnsupportedEncodingException { + final long start = SystemClock.elapsedRealtime(); + + final byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen); + fAssertNotNull("Hash result is non-null", key); + + final long end = SystemClock.elapsedRealtime(); + dumpLog(LOGTAG, "SHA-256 " + c + " took " + (end - start) + "ms"); + + if (expectedStr == null) { + return; + } + + fAssertEquals("Hash result is the appropriate length", dkLen, + Utils.hex2Byte(expectedStr).length); + assertExpectedBytes(expectedStr, key); + } + + private void assertExpectedBytes(final String expectedStr, byte[] key) { + fAssertEquals("Expected string matches hash result", expectedStr, Utils.byte2Hex(key)); + final byte[] expected = Utils.hex2Byte(expectedStr); + + fAssertEquals("Expected byte array length matches key length", expected.length, key.length); + fAssertArrayEquals("Expected byte array matches key byte array", expected, key); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java new file mode 100644 index 000000000..d9b014c1a --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testNewTab.java @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Element; +import org.mozilla.gecko.R; + +import android.app.Activity; +import android.view.View; + +import com.robotium.solo.Condition; + +/* A simple test that creates 2 new tabs and checks that the tab count increases. */ +public class testNewTab extends BaseTest { + private Element tabCount = null; + private Element tabs = null; + private final Element closeTab = null; + private int tabCountInt = 0; + + public void testNewTab() { + String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + String url2 = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + + blockForGeckoReady(); + + Activity activity = getActivity(); + tabCount = mDriver.findElement(activity, R.id.tabs_counter); + tabs = mDriver.findElement(activity, R.id.tabs); + mAsserter.ok(tabCount != null && tabs != null, + "Checking elements", "all elements present"); + + int expectedTabCount = 1; + getTabCount(expectedTabCount); + mAsserter.is(tabCountInt, expectedTabCount, "Initial number of tabs correct"); + + addTab(url); + expectedTabCount++; + getTabCount(expectedTabCount); + mAsserter.is(tabCountInt, expectedTabCount, "Number of tabs increased"); + + addTab(url2); + expectedTabCount++; + getTabCount(expectedTabCount); + mAsserter.is(tabCountInt, expectedTabCount, "Number of tabs increased"); + + // cleanup: close all opened tabs + closeAddedTabs(); + } + + private void getTabCount(final int expected) { + waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + String newTabCountText = tabCount.getText(); + tabCountInt = Integer.parseInt(newTabCountText); + if (tabCountInt == expected) { + return true; + } + return false; + } + }, MAX_WAIT_MS); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java new file mode 100644 index 000000000..434594fee --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testOSLocale.java @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.Locale; + +import org.mozilla.gecko.BrowserLocaleManager; +import org.mozilla.gecko.GeckoSharedPrefs; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.Locales; +import org.mozilla.gecko.PrefsHelper; + +import android.content.SharedPreferences; + + +public class testOSLocale extends BaseTest { + @Override + public void setUp() throws Exception { + super.setUp(); + + // Clear per-profile SharedPreferences as a workaround for Bug 1069687. + // We're trying to exercise logic that only applies on first onCreate! + // We can't rely on this occurring prior to the first broadcast, though, + // so see the main test method for more logic. + final String profileName = getTestProfile().getName(); + mAsserter.info("Setup", "Clearing pref in " + profileName + "."); + GeckoSharedPrefs.forProfileName(getActivity(), profileName) + .edit() + .remove("osLocale") + .apply(); + } + + public static class PrefState extends PrefsHelper.PrefHandlerBase { + private static final String PREF_LOCALE_OS = "intl.locale.os"; + private static final String PREF_ACCEPT_LANG = "intl.accept_languages"; + + private static final String[] TO_FETCH = {PREF_LOCALE_OS, PREF_ACCEPT_LANG}; + + public volatile String osLocale; + public volatile String acceptLanguages; + + private final Object waiter = new Object(); + + public void fetch() throws InterruptedException { + // Wait for any pending changes to have taken. Bug 1092580. + GeckoThread.waitOnGecko(); + synchronized (waiter) { + PrefsHelper.getPrefs(TO_FETCH, this); + waiter.wait(MAX_WAIT_MS); + } + } + + @Override + public void prefValue(String pref, String value) { + switch (pref) { + case PREF_LOCALE_OS: + osLocale = value; + return; + case PREF_ACCEPT_LANG: + acceptLanguages = value; + return; + } + } + + @Override + public void finish() { + synchronized (waiter) { + waiter.notify(); + } + } + } + + public void testOSLocale() throws Exception { + blockForDelayedStartup(); + + final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getActivity()); + final PrefState state = new PrefState(); + + state.fetch(); + + // We don't know at this point whether we were run against a dirty profile or not. + // + // If we cleared the pref above prior to BrowserApp's delayed init, or our Gecko + // profile has been used before, then we're already going to be set up for en-US. + // + // If we cleared the pref after the initial broadcast, and our Android-side profile + // has been used before but the Gecko profile is clean, then the Gecko prefs won't + // have been set. + // + // Instead, we always send a new locale code, and see what we get. + final Locale fr = Locales.parseLocaleCode("fr"); + BrowserLocaleManager.storeAndNotifyOSLocale(prefs, fr); + + state.fetch(); + + mAsserter.is(state.osLocale, "fr", "We're in fr."); + + // Now we can see what the expected Accept-Languages header should be. + // The OS locale is 'fr', so we have our app locale (en-US), + // the OS locale (fr), then any remaining fallbacks from intl.properties. + mAsserter.is(state.acceptLanguages, "en-us,fr,en", "We have the default en-US+fr Accept-Languages."); + + // Now set the app locale to be es-ES. + BrowserLocaleManager.getInstance().setSelectedLocale(getActivity(), "es-ES"); + + state.fetch(); + + mAsserter.is(state.osLocale, "fr", "We're still in fr."); + + // The correct set here depends on whether the + // browser was built with multiple locales or not. + // This is exasperating, but hey. + final boolean isMultiLocaleBuild = false; + + // This never changes. + final String SELECTED_LOCALES = "es-es,fr,"; + + // Expected, from es-ES's intl.properties: + final String EXPECTED = SELECTED_LOCALES + + (isMultiLocaleBuild ? "es,en-us,en" : // Expected, from es-ES's intl.properties. + "en-us,en"); // Expected, from en-US (the default). + + mAsserter.is(state.acceptLanguages, EXPECTED, "We have the right es-ES+fr Accept-Languages for this build."); + + // And back to en-US. + final Locale en_US = Locales.parseLocaleCode("en-US"); + BrowserLocaleManager.storeAndNotifyOSLocale(prefs, en_US); + BrowserLocaleManager.getInstance().resetToSystemLocale(getActivity()); + + state.fetch(); + + mAsserter.is(state.osLocale, "en-US", "We're in en-US."); + mAsserter.is(state.acceptLanguages, "en-us,en", "We have the default processed en-US Accept-Languages."); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java new file mode 100644 index 000000000..e7d402607 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPanCorrectness.java @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.PaintedSurface; + +/** + * A basic panning correctness test. + * - Loads a page and verifies it draws + * - drags page upwards by 100 pixels and verifies it draws + * - drags page leftwards by 100 pixels and verifies it draws + */ +public class testPanCorrectness extends PixelTest { + public void testPanCorrectness() { + String url = getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL); + + MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop()); + + blockForGeckoReady(); + + // load page and check we're at 0,0 + loadAndVerifyBoxes(url); + + // drag page upwards by 100 pixels + Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint(); + meh.dragSync(10, 150, 10, 50); + PaintedSurface painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + checkScrollWithBoxes(painted, 0, 100); + } finally { + painted.close(); + } + + // drag page leftwards by 100 pixels + paintExpecter = mActions.expectPaint(); + meh.dragSync(150, 10, 50, 10); + painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + checkScrollWithBoxes(painted, 100, 100); + } finally { + painted.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java new file mode 100644 index 000000000..65a4eaba6 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordEncrypt.java @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; + +import org.json.JSONObject; +import org.mozilla.gecko.NSSBridge; +import org.mozilla.gecko.db.BrowserContract; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +public class testPasswordEncrypt extends BaseTest { + public void testPasswordEncrypt() { + Context context = (Context)getActivity(); + ContentResolver cr = context.getContentResolver(); + mAsserter.isnot(cr, null, "Found a content resolver"); + ContentValues cvs = new ContentValues(); + + blockForGeckoReady(); + + File db = new File(mProfile, "signons.sqlite"); + String dbPath = db.getPath(); + + Uri passwordUri; + cvs.put("hostname", "http://www.example.com"); + cvs.put("encryptedUsername", "username"); + cvs.put("encryptedPassword", "password"); + + // Attempt to insert into the db + passwordUri = BrowserContract.Passwords.CONTENT_URI; + Uri.Builder builder = passwordUri.buildUpon(); + passwordUri = builder.appendQueryParameter("profilePath", mProfile).build(); + + Uri uri = cr.insert(passwordUri, cvs); + Uri expectedUri = passwordUri.buildUpon().appendPath("1").build(); + mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri"); + + Cursor list = mActions.querySql(dbPath, "SELECT encryptedUsername FROM moz_logins"); + list.moveToFirst(); + String decryptedU = null; + try { + decryptedU = NSSBridge.decrypt(context, mProfile, list.getString(0)); + } catch (Exception e) { + mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag? + } + mAsserter.is(decryptedU, "username", "Username was encrypted correctly when inserting"); + + list = mActions.querySql(dbPath, "SELECT encryptedPassword, encType FROM moz_logins"); + list.moveToFirst(); + String decryptedP = null; + try { + decryptedP = NSSBridge.decrypt(context, mProfile, list.getString(0)); + } catch (Exception e) { + mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag? + } + mAsserter.is(decryptedP, "password", "Password was encrypted correctly when inserting"); + mAsserter.is(list.getInt(1), 1, "Password has correct encryption type"); + + cvs.put("encryptedUsername", "username2"); + cvs.put("encryptedPassword", "password2"); + cr.update(passwordUri, cvs, null, null); + + list = mActions.querySql(dbPath, "SELECT encryptedUsername FROM moz_logins"); + list.moveToFirst(); + try { + decryptedU = NSSBridge.decrypt(context, mProfile, list.getString(0)); + } catch (Exception e) { + mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag? + } + mAsserter.is(decryptedU, "username2", "Username was encrypted when updating"); + + list = mActions.querySql(dbPath, "SELECT encryptedPassword FROM moz_logins"); + list.moveToFirst(); + try { + decryptedP = NSSBridge.decrypt(context, mProfile, list.getString(0)); + } catch (Exception e) { + mAsserter.ok(false, "NSSBridge.decrypt through Exception " + e, ""); // TODO: What is diag? + } + mAsserter.is(decryptedP, "password2", "Password was encrypted when updating"); + + // Trying to store a password while master password is enabled should throw, + // but because Android can't send Exceptions across processes + // it just results in a null uri/cursor being returned. + toggleMasterPassword("password"); + try { + uri = cr.insert(passwordUri, cvs); + // TODO: restore this assertion -- see bug 764901 + // mAsserter.is(uri, null, "Storing a password while MP was set should fail"); + + Cursor c = cr.query(passwordUri, null, null, null, null); + // TODO: restore this assertion -- see bug 764901 + // mAsserter.is(c, null, "Querying passwords while MP was set should fail"); + } catch (Exception ex) { + // Password provider currently can not throw across process + // so we should not catch this exception here + mAsserter.ok(false, "Caught exception", ex.toString()); + } + toggleMasterPassword("password"); + } + + private void toggleMasterPassword(String passwd) { + setPreferenceAndWaitForChange("privacy.masterpassword.enabled", passwd); + } + + @Override + public void tearDown() throws Exception { + // remove the entire signons.sqlite file + File profile = new File(mProfile); + File db = new File(profile, "signons.sqlite"); + if (db.delete()) { + mAsserter.dumpLog("tearDown deleted "+db.toString()); + } else { + mAsserter.dumpLog("tearDown did not delete "+db.toString()); + } + + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java new file mode 100644 index 000000000..8a2cc357e --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPasswordProvider.java @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.io.File; + +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.GeckoDisabledHosts; +import org.mozilla.gecko.db.BrowserContract.Passwords; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +/** + * A basic password contentprovider test. + * - inserts a password when the database is not yet set up + * - inserts a password + * - updates a password + * - deletes a password + * - inserts a disabled host + * - queries for disabled host + */ +public class testPasswordProvider extends BaseTest { + private static final String DB_NAME = "signons.sqlite"; + + public void testPasswordProvider() { + Context context = (Context)getActivity(); + ContentResolver cr = context.getContentResolver(); + ContentValues[] cvs = new ContentValues[1]; + cvs[0] = new ContentValues(); + + blockForGeckoReady(); + + cvs[0].put("hostname", "http://www.example.com"); + cvs[0].put("httpRealm", "http://www.example.com"); + cvs[0].put("formSubmitURL", "http://www.example.com"); + cvs[0].put("usernameField", "usernameField"); + cvs[0].put("passwordField", "passwordField"); + cvs[0].put("encryptedUsername", "username"); + cvs[0].put("encryptedPassword", "password"); + cvs[0].put("encType", "1"); + + // Attempt to insert into the db + Uri passwordUri = Passwords.CONTENT_URI; + Uri.Builder builder = passwordUri.buildUpon(); + passwordUri = builder.appendQueryParameter("profilePath", mProfile).build(); + + Uri uri = cr.insert(passwordUri, cvs[0]); + Uri expectedUri = passwordUri.buildUpon().appendPath("1").build(); + mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri"); + Cursor c = cr.query(passwordUri, null, null, null, null); + SqliteCompare(c, cvs); + + cvs[0].put("usernameField", "usernameField2"); + cvs[0].put("passwordField", "passwordField2"); + + int numUpdated = cr.update(passwordUri, cvs[0], null, null); + mAsserter.is(1, numUpdated, "Correct number updated"); + c = cr.query(passwordUri, null, null, null, null); + SqliteCompare(c, cvs); + + int numDeleted = cr.delete(passwordUri, null, null); + mAsserter.is(1, numDeleted, "Correct number deleted"); + cvs = new ContentValues[0]; + c = cr.query(passwordUri, null, null, null, null); + SqliteCompare(c, cvs); + + ContentValues values = new ContentValues(); + values.put("hostname", "http://www.example.com"); + + // Attempt to insert into the db. + Uri disabledHostUri = GeckoDisabledHosts.CONTENT_URI; + builder = disabledHostUri.buildUpon(); + disabledHostUri = builder.appendQueryParameter("profilePath", mProfile).build(); + + uri = cr.insert(disabledHostUri, values); + expectedUri = disabledHostUri.buildUpon().appendPath("1").build(); + mAsserter.is(uri.toString(), expectedUri.toString(), "Insert returned correct uri"); + Cursor cursor = cr.query(disabledHostUri, null, null, null, null); + assertNotNull(cursor); + assertEquals(1, cursor.getCount()); + cursor.moveToFirst(); + CursorMatches(cursor, values); + } + + @Override + public void tearDown() throws Exception { + // remove the entire signons.sqlite file + File profile = new File(mProfile); + File db = new File(profile, "signons.sqlite"); + if (db.delete()) { + mAsserter.dumpLog("tearDown deleted "+db.toString()); + } else { + mAsserter.dumpLog("tearDown did not delete "+db.toString()); + } + + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java new file mode 100644 index 000000000..e4d997895 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPermissions.java @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.ArrayList; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.PaintedSurface; + +import android.widget.CheckBox; + +public class testPermissions extends PixelTest { + public void testPermissions() { + blockForGeckoReady(); + + geolocationTest(); + } + + private void geolocationTest() { + Actions.RepeatedEventExpecter paintExpecter; + + // Test geolocation notification + loadAndPaint(getAbsoluteUrl(mStringHelper.ROBOCOP_GEOLOCATION_URL)); + waitForText("wants your location"); + + // Uncheck the "Don't ask again for this site" checkbox + ArrayList<CheckBox> checkBoxes = mSolo.getCurrentViews(CheckBox.class); + mAsserter.ok(checkBoxes.size() == 1, "checkbox count", "only one checkbox visible"); + mAsserter.ok(mSolo.isCheckBoxChecked(0), "checkbox checked", "checkbox is checked"); + mSolo.clickOnCheckBox(0); + mAsserter.ok(!mSolo.isCheckBoxChecked(0), "checkbox not checked", "checkbox is not checked"); + + // Test "Share" button functionality with unchecked checkbox + paintExpecter = mActions.expectPaint(); + mSolo.clickOnText("Share"); + PaintedSurface painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green"); + } finally { + painted.close(); + } + + // Re-trigger geolocation notification + reloadAndPaint(); + waitForText("wants your location"); + + // Make sure the checkbox is checked this time + mAsserter.ok(mSolo.isCheckBoxChecked(0), "checkbox checked", "checkbox is checked"); + + // Test "Share" button functionality with checked checkbox + paintExpecter = mActions.expectPaint(); + mSolo.clickOnText("Share"); + painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green"); + } finally { + painted.close(); + } + + // When we reload the page, location should be automatically shared + painted = reloadAndGetPainted(); + try { + mAsserter.ispixel(painted.getPixelAt(10, 10), 0, 0x80, 0, "checking page background is green"); + } finally { + painted.close(); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java new file mode 100644 index 000000000..1461fd9be --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPictureLinkContextMenu.java @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +public class testPictureLinkContextMenu extends ContentContextMenuTest { + + // Test website strings + private static String PICTURE_PAGE_URL; + private static String BLANK_PAGE_URL; + private static String PICTURE_URL; + private static final String tabs [] = { "Image", "Link" }; + private static final String photoMenuItems [] = { "Copy Image Location", "Share Image", "View Image", "Set Image As", "Save Image" }; + private static final String imageTitle = "^Image$"; + + public void testPictureLinkContextMenu() { + final String PICTURE_PAGE_TITLE = mStringHelper.ROBOCOP_PICTURE_LINK_TITLE; + final String linkMenuItems [] = mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB; + + blockForGeckoReady(); + + PICTURE_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_PICTURE_LINK_URL); + BLANK_PAGE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + PICTURE_URL=getAbsoluteUrl(mStringHelper.ROBOCOP_PICTURE_URL); + loadAndPaint(PICTURE_PAGE_URL); + verifyUrlInContentDescription(PICTURE_PAGE_URL); + + switchTabs(imageTitle); + verifyContextMenuItems(photoMenuItems); + verifyTabs(tabs); + switchTabs(imageTitle); + verifyCopyOption(photoMenuItems[0], "Firefox.jpg"); // Test the "Copy Image Location" option + switchTabs(imageTitle); + verifyShareOption(photoMenuItems[1], PICTURE_PAGE_TITLE); // Test the "Share Image" option + switchTabs(imageTitle); + verifyViewImageOption(photoMenuItems[2], PICTURE_URL, PICTURE_PAGE_TITLE); // Test the "View Image" option + + verifyContextMenuItems(linkMenuItems); + openTabFromContextMenu(linkMenuItems[0],2); // Test the "Open in New Tab" option - expecting 2 tabs: the original and the new one + openTabFromContextMenu(linkMenuItems[1],2); // Test the "Open in Private Tab" option - expecting only 2 tabs in normal mode + verifyCopyOption(linkMenuItems[2], BLANK_PAGE_URL); // Test the "Copy Link" option + verifyShareOption(linkMenuItems[3], PICTURE_PAGE_TITLE); // Test the "Share Link" option + verifyBookmarkLinkOption(linkMenuItems[4],BLANK_PAGE_URL); // Test the "Bookmark Link" option + } + + @Override + public void tearDown() throws Exception { + mDatabaseHelper.deleteBookmark(BLANK_PAGE_URL); + super.tearDown(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java new file mode 100644 index 000000000..f63358d57 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrefsObserver.java @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; + +/** + * Basic test to check bounce-back from overscroll. + * - Load the page and verify it draws + * - Drag page downwards by 100 pixels into overscroll, verify it snaps back. + * - Drag page rightwards by 100 pixels into overscroll, verify it snaps back. + */ +public class testPrefsObserver extends BaseTest { + private static final String PREF_TEST_PREF = "robocop.tests.dummy"; + + private Actions.PrefWaiter prefWaiter; + private boolean prefValue; + + public void setPref(boolean value) { + mAsserter.dumpLog("Setting pref"); + mActions.setPref(PREF_TEST_PREF, value, /* flush */ false); + } + + public void waitAndCheckPref(boolean value) { + mAsserter.dumpLog("Waiting to check pref"); + + mAsserter.isnot(prefWaiter, null, "Check pref waiter is not null"); + prefWaiter.waitForFinish(); + + mAsserter.is(prefValue, value, "Check correct pref value"); + } + + public void verifyDisconnect() { + mAsserter.dumpLog("Checking pref observer is removed"); + + final boolean newValue = !prefValue; + setPreferenceAndWaitForChange(PREF_TEST_PREF, newValue); + mAsserter.isnot(prefValue, newValue, "Check pref value did not change"); + } + + public void observePref() { + mAsserter.dumpLog("Setting up pref observer"); + + // Setup the pref observer + mAsserter.is(prefWaiter, null, "Check pref waiter is null"); + prefWaiter = mActions.addPrefsObserver( + new String[] { PREF_TEST_PREF }, new Actions.PrefHandlerBase() { + @Override // Actions.PrefHandlerBase + public void prefValue(String pref, boolean value) { + mAsserter.is(pref, PREF_TEST_PREF, "Check correct pref name"); + prefValue = value; + } + }); + } + + public void removePrefObserver() { + mAsserter.dumpLog("Removing pref observer"); + + mActions.removePrefsObserver(prefWaiter); + } + + public void testPrefsObserver() { + blockForGeckoReady(); + + setPref(false); + observePref(); + waitAndCheckPref(false); + + setPref(true); + waitAndCheckPref(true); + + removePrefObserver(); + verifyDisconnect(); + + // Removing again should be a no-op. + removePrefObserver(); + } +} + diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java new file mode 100644 index 000000000..461e95aa7 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.ArrayList; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.Tabs; + +/** + * The test loads a new private tab and loads a page with a big link on it + * Opens the link in a new private tab and checks that it is private + * Adds a new normal tab and loads a 3rd URL + * Checks that the bigLinkUrl loaded in the normal tab is present in the browsing history but the 2 urls opened in private tabs are not + */ +public class testPrivateBrowsing extends ContentContextMenuTest { + + public void testPrivateBrowsing() { + String bigLinkUrl = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL); + String blank1Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + String blank2Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + Tabs tabs = Tabs.getInstance(); + + blockForGeckoReady(); + + Actions.EventExpecter tabExpecter = mActions.expectGeckoEvent("Tab:Added"); + Actions.EventExpecter contentExpecter = mActions.expectGeckoEvent("Content:PageShow"); + tabs.loadUrl(bigLinkUrl, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE); + tabExpecter.blockForEvent(); + tabExpecter.unregisterListener(); + contentExpecter.blockForEvent(); + contentExpecter.unregisterListener(); + verifyTabCount(1); + + // May intermittently get context menu for normal tab without additional wait + mSolo.sleep(5000); + + // Open the link context menu and verify the options + verifyContextMenuItems(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB); + + // Check that "Open Link in New Tab" is not in the menu + mAsserter.ok(!mSolo.searchText(mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]), "Checking that 'Open Link in New Tab' is not displayed in the context menu", "'Open Link in New Tab' is not displayed in the context menu"); + + // Open the link in a new private tab and check that it is private + tabExpecter = mActions.expectGeckoEvent("Tab:Added"); + contentExpecter = mActions.expectGeckoEvent("Content:PageShow"); + mSolo.clickOnText(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB[0]); + String eventData = tabExpecter.blockForEventData(); + tabExpecter.unregisterListener(); + contentExpecter.blockForEvent(); + contentExpecter.unregisterListener(); + mAsserter.ok(isTabPrivate(eventData), "Checking if the new tab opened from the context menu was a private tab", "The tab was a private tab"); + verifyTabCount(2); + + // Open a normal tab to check later that it was registered in the Firefox Browser History + tabExpecter = mActions.expectGeckoEvent("Tab:Added"); + contentExpecter = mActions.expectGeckoEvent("Content:PageShow"); + tabs.loadUrl(blank2Url, Tabs.LOADURL_NEW_TAB); + tabExpecter.blockForEvent(); + tabExpecter.unregisterListener(); + contentExpecter.blockForEvent(); + contentExpecter.unregisterListener(); + verifyTabCount(2); + + // wait for history updates to complete + mSolo.sleep(3000); + + // Get the history list and check that the links open in private browsing are not saved + final ArrayList<String> firefoxHistory = mDatabaseHelper.getBrowserDBUrls(DatabaseHelper.BrowserDataType.HISTORY); + + mAsserter.ok(!firefoxHistory.contains(bigLinkUrl), "Check that the link opened in the first private tab was not saved", bigLinkUrl + " was not added to history"); + mAsserter.ok(!firefoxHistory.contains(blank1Url), "Check that the link opened in the private tab from the context menu was not saved", blank1Url + " was not added to history"); + mAsserter.ok(firefoxHistory.contains(blank2Url), "Check that the link opened in the normal tab was saved", blank2Url + " was added to history"); + } + + private boolean isTabPrivate(String eventData) { + try { + JSONObject data = new JSONObject(eventData); + return data.getBoolean("isPrivate"); + } catch (JSONException e) { + mAsserter.ok(false, "Error parsing the event data", e.toString()); + return false; + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java new file mode 100644 index 000000000..f645fe3be --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPromptGridInput.java @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +public class testPromptGridInput extends BaseTest { + protected int index = 1; + public void testPromptGridInput() { + blockForGeckoReady(); + + test(1); + + testGridItem("Icon 1"); + testGridItem("Icon 2"); + testGridItem("Icon 3"); + testGridItem("Icon 4"); + testGridItem("Icon 5"); + testGridItem("Icon 6"); + testGridItem("Icon 7"); + testGridItem("Icon 8"); + testGridItem("Icon 9"); + testGridItem("Icon 10"); + testGridItem("Icon 11"); + + mSolo.clickOnText("Icon 11"); + mSolo.clickOnText("OK"); + + mAsserter.ok(waitForText("PASS"), "test passed", "PASS"); + mSolo.goBack(); + } + + public void testGridItem(String title) { + // Force the list to scroll if necessary + mSolo.waitForText(title, 1, 500, true); + mAsserter.ok(waitForText(title), "Found grid item", title); + } + + public void test(final int num) { + // Load about:blank between each test to ensure we reset state + loadUrl(mStringHelper.ABOUT_BLANK_URL); + mAsserter.ok(waitForText(mStringHelper.ABOUT_BLANK_URL), "Loaded blank page", + mStringHelper.ABOUT_BLANK_URL); + + loadUrl("chrome://roboextender/content/robocop_prompt_gridinput.html#test" + num); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java new file mode 100644 index 000000000..6dbc70de5 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderCacheMigration.java @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.db.BrowserDatabaseHelper; + +import java.io.File; +import java.io.IOException; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +/** + * Tests that our readercache-migration works correctly. + * + * Our main concern is ensuring that the hashed path for a given url is the same in Java + * as it was in JS, or else our (Java-based) migration will lose track of valid cached items. + */ +public class testReaderCacheMigration extends JavascriptBridgeTest { + + private final String[] TEST_DOMAINS = new String[] { + "", + "http://mozilla.org", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1234315#c41", + "http://www.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.com/" + }; + + private static final String TEST_JS = "testReaderCacheMigration.js"; + + /** + * We compute the path-name in Java, and pass this through to JS, which conducts the actual + * equality check. Our JavascriptBridge doesn't seem to support return values, so we need + * to instead pass the computed path-name in at least one direction. + */ + private void checkPathMatches(final String pageURL, final File cacheDir) { + final String hashedName = BrowserDatabaseHelper.getReaderCacheFileNameForURL(pageURL); + + final File cacheFile = new File(cacheDir, hashedName); + + try { + // We have to use the canonical path to match what the JS side will use. We could + // instead just match on the file name, and not the path, but this helps + // ensure that we've not broken any of the path finding either. + getJS().syncCall("check_hashed_path_matches", pageURL, cacheFile.getCanonicalPath()); + } catch (IOException e) { + fAssertTrue("Unable to getCanonicalPath(), this should never happen", false); + } + + } + + public void testReaderCacheMigration() { + blockForReadyAndLoadJS(TEST_JS); + + final File cacheDir = new File(GeckoProfile.get(getActivity()).getDir(), "readercache"); + + for (final String URL : TEST_DOMAINS) { + checkPathMatches(URL, cacheDir); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java new file mode 100644 index 000000000..31e012070 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReaderModeTitle.java @@ -0,0 +1,19 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +/** + * This tests ensures that the toolbar in reader mode displays the original page url. + */ +public class testReaderModeTitle extends UITest { + public void testReaderModeTitle() { + GeckoHelper.blockForReady(); + + NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE); + + mToolbar.pressReaderModeButton(); + + mToolbar.assertTitle(mStringHelper.ROBOCOP_READER_MODE_BASIC_ARTICLE); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java new file mode 100644 index 000000000..2006bbbfc --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListCache.java @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + + +public class testReadingListCache extends JavascriptTest { + public testReadingListCache() { + super("testReadingListCache.js"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java new file mode 100644 index 000000000..dc181defc --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testReadingListToBookmarksMigration.java @@ -0,0 +1,217 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.util.Log; + +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.GeckoProfileDirectories; +import org.mozilla.gecko.db.*; +import org.mozilla.gecko.reader.SavedReaderViewHelper; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import static org.mozilla.gecko.db.BrowserContract.*; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +// TODO: Move to junit 3 tests, once those run in automation. There is no ui testing to do so it's a better fit. + +/** + * This test runs the 30 to 31 database upgrade, which moves reading-list INPUT_FILES from a separate + * reading-list folder into mobile bookmarks. + * + * It is based on testBrowserDatabaseHelperUpgrades. We load a v30 db containing two reading list + * INPUT_FILES, and test that these have successfully been converted into bookmarks. + */ +public class testReadingListToBookmarksMigration extends UITest { + private ArrayList<File> tempFiles; + + // These names are generated by hashing the URLs, see INPUT_URLS below, and + // BrowserDatabaseHelper.getReaderCacheFileNameForURL() + private static final ArrayList<String> INPUT_FILES = new ArrayList<String>() {{ + add("DWUP3U4ERC6TKJVSYXKJLHHEFY.json"); + add("KWNV7PXD3JFOJBQJVFXI3CQKNE.json"); + }}; + + // same ordering as in INPUT_FILES, although we don't rely on ordering in this test + private static final ArrayList<String> INPUT_URLS = new ArrayList<String>() {{ + add("http://www.bbc.com/news/election-us-2016-35962179"); + add("http://www.bbc.com/news/world-europe-35962670"); + }}; + + @Override + public void setUp() throws Exception { + super.setUp(); + // TODO: We already install & remove the profile directory each run so it'd be safer for clean-up to store + // this there. That being said, temporary files are still stored in the application directory so these temporary + // files will get cleaned up when the application is uninstalled or when data is cleared. + tempFiles = new ArrayList<>(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + for (final File file : tempFiles) { + file.delete(); + } + } + + private void walkRLPreMigration(SQLiteDatabase db) { + Set<String> urls = new HashSet<>(INPUT_URLS); + + final Cursor c = db.rawQuery("SELECT * FROM " + ReadingListItems.TABLE_NAME, null); + + fAssertNotNull("Cursor cannot be null", c); + try { + final boolean movedToFirst = c.moveToFirst(); + fAssertTrue("Cursor must have data", movedToFirst); + + int urlIndex = c.getColumnIndexOrThrow(ReadingListItems.URL); + do { + final String url = c.getString(urlIndex); + + boolean removed = urls.remove(url); + fAssertTrue("Unexpected reading-list URL in database", removed); + } while (c.moveToNext()); + } finally { + c.close(); + } + + fAssertTrue("All urls should have been removed from set", urls.isEmpty()); + } + + private void walkRLPostMigration(SQLiteDatabase db) { + Set<String> urls = new HashSet<>(INPUT_URLS); + + final Cursor c = db.rawQuery("SELECT * FROM " + + Bookmarks.VIEW_WITH_ANNOTATIONS + + " WHERE " + BrowserContract.UrlAnnotations.KEY + " = ?", + new String[] { + BrowserContract.UrlAnnotations.Key.READER_VIEW.getDbValue() + }); + + fAssertNotNull("Cursor cannot be null", c); + try { + final boolean movedToFirst = c.moveToFirst(); + fAssertTrue("Cursor must have data", movedToFirst); + + int urlIndex = c.getColumnIndexOrThrow(Bookmarks.URL); + do { + final String url = c.getString(urlIndex); + + boolean removed = urls.remove(url); + fAssertTrue("Unexpected reading-list URL in database", removed); + } while (c.moveToNext()); + } finally { + c.close(); + } + + fAssertTrue("All urls should have been removed from set", urls.isEmpty()); + } + + /** + * @throws IOException if the database or input files cannot be copied. + */ + public void testReadingListToBookmarksMigration() throws IOException { + final String tempDbPath = copyAssets(); + final SQLiteDatabase db = SQLiteDatabase.openDatabase(tempDbPath, null, 0); + + try { + // This initialises the helper, but does not open the DB. + BrowserDatabaseHelper dbHelper = new BrowserDatabaseHelper(getActivity(), tempDbPath); + + walkRLPreMigration(db); + + // Run just one upgrade - we don't know what future upgrades might do, whereas with one + // upgrade we can guarantee a given DB state. + dbHelper.onUpgrade(db, 30, 31); + + // SavedReaderViewHelper writes annotations directly to the GeckoProfile DB (as opposed + // to our local DB copy). We aren't able to read this here (and the data isn't written + // to our own db), hence we can't test the DB content yet. +// walkRLPostMigration(db); + + SavedReaderViewHelper rvh = SavedReaderViewHelper.getSavedReaderViewHelper(getActivity()); + + fAssertEquals("All input files should have been migrated", INPUT_FILES.size(), rvh.size()); + for (String url : INPUT_URLS) { + boolean isCached = rvh.isURLCached(url); + fAssertTrue("URL no longer in cache after migration", isCached); + } + } finally { + db.close(); + } + } + + private void copyAssetToFile(String inputPath, File destination) throws IOException { + final InputStream inputStream = openFileFromAssets(inputPath); + final OutputStream os = new BufferedOutputStream(new FileOutputStream(destination)); + try { + final byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) > 0) { + os.write(buffer, 0, len); + } + os.flush(); + } finally { + os.close(); + inputStream.close(); + } + } + + /** + * Copies assets into the desired locations. We need to copy our DB into a temporary file, + * and readercache items into the profile directory. + * + * @throws IOException if reading the existing files or writing the temporary files fails + */ + private String copyAssets() throws IOException { + final File profileDir = GeckoProfile.get(getActivity()).getDir(); + final File cacheDir = new File(profileDir, "readercache"); + cacheDir.mkdir(); + + for (String name : INPUT_FILES) { + final String path = "readercache" + File.separator + name; + final File destination = new File(cacheDir, name); + tempFiles.add(destination); + + Log.d(LOGTAG, "Moving readerview cache file to " + destination.getPath()); + copyAssetToFile(path, destination); + } + + final File dbDestination = File.createTempFile("temporaryDB_", "db"); + tempFiles.add(dbDestination); + + Log.d(LOGTAG, "Moving DB from assets to " + dbDestination.getPath()); + copyAssetToFile("browser.db", dbDestination); + + return dbDestination.getPath(); + } + + private InputStream openFileFromAssets(final String name) throws IOException { + final String assetPath = String.format("reading_list_bookmarks_migration" + File.separator + name); + Log.d(LOGTAG, "Opening file from assets: " + assetPath); + try { + return new BufferedInputStream(getInstrumentation().getContext().getAssets().open(assetPath)); + } catch (final FileNotFoundException e) { + throw new IllegalStateException("Declared input files must be provided", e); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java new file mode 100644 index 000000000..8977aa177 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRestrictions.java @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse; +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; + +import org.mozilla.gecko.restrictions.Restrictable; +import org.mozilla.gecko.restrictions.Restrictions; +import org.mozilla.gecko.tests.helpers.GeckoHelper; + +public class testRestrictions extends UITest { + public void testRestrictions() { + GeckoHelper.blockForReady(); + + // No restrictions should be enforced when using a normal profile + for (Restrictable restrictable : Restrictable.values()) { + if (restrictable == Restrictable.BLOCK_LIST) { + assertFeatureDisabled(restrictable); + } else { + assertFeatureEnabled(restrictable); + } + } + } + + private void assertFeatureEnabled(Restrictable restrictable) { + fAssertTrue(String.format("Restrictable feature %s is enabled", restrictable.name), + Restrictions.isAllowed(getActivity(), restrictable) + ); + } + + private void assertFeatureDisabled(Restrictable restrictable) { + fAssertFalse(String.format("Restrictable feature %s is disabled", restrictable.name), + Restrictions.isAllowed(getActivity(), restrictable) + ); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java new file mode 100644 index 000000000..df192fc43 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.NativeEventListener; +import org.mozilla.gecko.util.NativeJSObject; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail; + +public class testRuntimePermissionsAPI extends JavascriptTest implements NativeEventListener { + public testRuntimePermissionsAPI() { + super("testRuntimePermissionsAPI.js"); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + EventDispatcher.getInstance().registerGeckoThreadListener(this, "RuntimePermissions:Prompt"); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "RuntimePermissions:Prompt"); + } + + @Override + public void handleMessage(String event, NativeJSObject message, EventCallback callback) { + mAsserter.is(event, "RuntimePermissions:Prompt", "Received RuntimePermissions:Prompt event"); + + try { + String[] permissions = message.getStringArray("permissions"); + mAsserter.is(3, permissions.length, "Received three permissions"); + + mAsserter.is("android.permission.CAMERA", permissions[0], "Received CAMERA permission"); + mAsserter.is("android.permission.WRITE_EXTERNAL_STORAGE", permissions[1], "Received WRITE_EXTERNAL_STORAGE permission"); + mAsserter.is("android.permission.RECORD_AUDIO", permissions[2], "Received RECORD_AUDIO permission"); + } catch (Exception e) { + fFail("Event does not contain expected data: " + e.getMessage()); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java new file mode 100644 index 000000000..3c22703bc --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchHistoryProvider.java @@ -0,0 +1,379 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.concurrent.Callable; + +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.SearchHistory; +import org.mozilla.gecko.db.SearchHistoryProvider; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +public class testSearchHistoryProvider extends ContentProviderTest { + + // Translations of "United Kingdom" in several different languages + private static final String[] testStrings = { + "An Ríocht Aontaithe", // Irish + "Angli", // Albanian + "Britanniarum Regnum", // Latin + "Britio", // Esperanto + "Büyük Britanya", // Turkish + "Egyesült Királyság", // Hungarian + "Erresuma Batua", // Basque + "Inggris Raya", // Indonesian + "Ir-Renju Unit", // Maltese + "Iso-Britannia", // Finnish + "Jungtinė Karalystė", // Lithuanian + "Lielbritānija", // Latvian + "Regatul Unit", // Romanian + "Regne Unit", // Catalan, Valencian + "Regno Unito", // Italian + "Royaume-Uni", // French + "Spojené království", // Czech + "Spojené kráľovstvo", // Slovak + "Storbritannia", // Norwegian + "Storbritannien", // Danish + "Suurbritannia", // Estonian + "Ujedinjeno Kraljevstvo", // Bosnian + "United Alaeze", // Igbo + "United Kingdom", // English + "Vereinigtes Königreich", // German + "Verenigd Koninkrijk", // Dutch + "Verenigde Koninkryk", // Afrikaans + "Vương quốc Anh", // Vietnamese + "Wayòm Ini", // Haitian, Haitian Creole + "Y Deyrnas Unedig", // Welsh + "Združeno kraljestvo", // Slovene + "Zjednoczone Królestwo", // Polish + "Ηνωμένο Βασίλειο", // Greek (modern) + "Великобритания", // Russian + "Нэгдсэн Вант Улс", // Mongolian + "Обединетото Кралство", // Macedonian + "Уједињено Краљевство", // Serbian + "Միացյալ Թագավորություն", // Armenian + "בריטניה", // Hebrew (modern) + "פֿאַראייניקטע מלכות", // Yiddish + "المملكة المتحدة", // Arabic + "برطانیہ", // Urdu + "پادشاهی متحده", // Persian (Farsi) + "यूनाइटेड किंगडम", // Hindi + "संयुक्त राज्य", // Nepali + "যুক্তরাজ্য", // Bengali, Bangla + "યુનાઇટેડ કિંગડમ", // Gujarati + "ஐக்கிய ராஜ்யம்", // Tamil + "สหราชอาณาจักร", // Thai + "ສະຫະປະຊາຊະອານາຈັກ", // Lao + "გაერთიანებული სამეფო", // Georgian + "イギリス", // Japanese + "联合王国" // Chinese + }; + + + private static final String DB_NAME = "searchhistory.db"; + + /** + * Boilerplate alert. + * <p/> + * Make sure this method is present and that it returns a new + * instance of your class. + */ + private static final Callable<ContentProvider> sProviderFactory = + new Callable<ContentProvider>() { + @Override + public ContentProvider call() { + return new SearchHistoryProvider(); + } + }; + + @Override + public void setUp() throws Exception { + super.setUp(sProviderFactory, BrowserContract.SEARCH_HISTORY_AUTHORITY, DB_NAME); + mTests.add(new TestInsert()); + mTests.add(new TestUnicodeQuery()); + mTests.add(new TestTimestamp()); + mTests.add(new TestLimit()); + mTests.add(new TestDelete()); + mTests.add(new TestIncrement()); + } + + public void testSearchHistory() throws Exception { + for (Runnable test : mTests) { + String testName = test.getClass().getSimpleName(); + setTestName(testName); + mAsserter.dumpLog( + "testBrowserProvider: Database empty - Starting " + testName + "."); + // Clear the db + mProvider.delete(SearchHistory.CONTENT_URI, null, null); + test.run(); + } + } + + /** + * Verify that we can pass a LIMIT clause using a query parameter. + */ + private class TestLimit extends TestCase { + @Override + public void test() throws Exception { + ContentValues cv; + for (int i = 0; i < testStrings.length; i++) { + cv = new ContentValues(); + cv.put(SearchHistory.QUERY, testStrings[i]); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + } + + final int limit = 5; + + // Test 1: Handle proper input. + + Uri uri = SearchHistory.CONTENT_URI + .buildUpon() + .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)) + .build(); + + Cursor c = mProvider.query(uri, null, null, null, null); + try { + mAsserter.is(c.getCount(), limit, + String.format("Should have %d results", limit)); + } finally { + c.close(); + } + + // Test 2: Empty input yields all results. + + uri = SearchHistory.CONTENT_URI + .buildUpon() + .appendQueryParameter(BrowserContract.PARAM_LIMIT, "") + .build(); + + c = mProvider.query(uri, null, null, null, null); + try { + mAsserter.is(c.getCount(), testStrings.length, "Should have all results"); + } finally { + c.close(); + } + + // Test 3: Illegal params. + + String[] illegalParams = new String[] {"a", "-1"}; + boolean success = true; + + for (String param : illegalParams) { + success = true; + + uri = SearchHistory.CONTENT_URI + .buildUpon() + .appendQueryParameter(BrowserContract.PARAM_LIMIT, param) + .build(); + + try { + c = mProvider.query(uri, null, null, null, null); + success = false; + } catch(IllegalArgumentException e) { + // noop. + } finally { + if (c != null) { + c.close(); + } + } + + mAsserter.ok(success, "LIMIT", param + " should have been an invalid argument"); + } + + } + } + + /** + * Verify that we can insert values into the DB, including unicode. + */ + private class TestInsert extends TestCase { + @Override + public void test() throws Exception { + ContentValues cv; + for (int i = 0; i < testStrings.length; i++) { + cv = new ContentValues(); + cv.put(SearchHistory.QUERY, testStrings[i]); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + } + + final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + mAsserter.is(c.getCount(), testStrings.length, + "Should have one row for each insert"); + } finally { + c.close(); + } + } + } + + /** + * Verify that we can insert values into the DB, including unicode. + */ + private class TestUnicodeQuery extends TestCase { + @Override + public void test() throws Exception { + final String selection = SearchHistory.QUERY + " = ?"; + + for (int i = 0; i < testStrings.length; i++) { + final ContentValues cv = new ContentValues(); + cv.put(SearchHistory.QUERY, testStrings[i]); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + + final Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, selection, + new String[]{ testStrings[i] }, null); + try { + mAsserter.is(c.getCount(), 1, + "Should have one row for insert of " + testStrings[i]); + } finally { + c.close(); + } + } + } + } + + /** + * Verify that timestamps are updated on insert. + */ + private class TestTimestamp extends TestCase { + @Override + public void test() throws Exception { + String insertedTerm = "Courtside Seats"; + long insertStart; + long insertFinish; + long t1Db; + long t2Db; + + ContentValues cv = new ContentValues(); + cv.put(SearchHistory.QUERY, insertedTerm); + + // First check that the DB has a value that is close to the + // system time. + insertStart = System.currentTimeMillis(); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + insertFinish = System.currentTimeMillis(); + + final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + c1.moveToFirst(); + t1Db = c1.getLong(c1.getColumnIndex(SearchHistory.DATE_LAST_VISITED)); + } finally { + c1.close(); + } + + mAsserter.dumpLog("First insert:"); + mAsserter.dumpLog(" insertStart " + insertStart); + mAsserter.dumpLog(" insertFinish " + insertFinish); + mAsserter.dumpLog(" t1Db " + t1Db); + mAsserter.ok(t1Db >= insertStart, "DATE_LAST_VISITED", + "Date last visited should be set on insert."); + mAsserter.ok(t1Db <= insertFinish, "DATE_LAST_VISITED", + "Date last visited should be set on insert."); + + cv = new ContentValues(); + cv.put(SearchHistory.QUERY, insertedTerm); + + insertStart = System.currentTimeMillis(); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + insertFinish = System.currentTimeMillis(); + + final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + c2.moveToFirst(); + t2Db = c2.getLong(c2.getColumnIndex(SearchHistory.DATE_LAST_VISITED)); + } finally { + c2.close(); + } + + mAsserter.dumpLog("Second insert:"); + mAsserter.dumpLog(" insertStart " + insertStart); + mAsserter.dumpLog(" insertFinish " + insertFinish); + mAsserter.dumpLog(" t2Db " + t2Db); + + mAsserter.ok(t2Db >= insertStart, "DATE_LAST_VISITED", + "Date last visited should be set on insert."); + mAsserter.ok(t2Db <= insertFinish, "DATE_LAST_VISITED", + "Date last visited should be set on insert."); + mAsserter.ok(t2Db >= t1Db, "DATE_LAST_VISITED", + "Date last visited should be updated on key increment."); + } + } + + /** + * Verify that sending a delete command empties the database. + */ + private class TestDelete extends TestCase { + @Override + public void test() throws Exception { + String insertedTerm = "Courtside Seats"; + + ContentValues cv = new ContentValues(); + cv.put(SearchHistory.QUERY, insertedTerm); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + + final Cursor c1 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + mAsserter.is(c1.getCount(), 1, "Should have one value"); + mProvider.delete(SearchHistory.CONTENT_URI, null, null); + } finally { + c1.close(); + } + + final Cursor c2 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + mAsserter.is(c2.getCount(), 0, "Should be empty"); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + } finally { + c2.close(); + } + + final Cursor c3 = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + mAsserter.is(c3.getCount(), 1, "Should have one value"); + } finally { + c3.close(); + } + } + } + + + /** + * Ensure that we only increment when the case matches. + */ + private class TestIncrement extends TestCase { + @Override + public void test() throws Exception { + ContentValues cv = new ContentValues(); + cv.put(SearchHistory.QUERY, "omaha"); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + + cv = new ContentValues(); + cv.put(SearchHistory.QUERY, "omaha"); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + + Cursor c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + c.moveToFirst(); + mAsserter.is(c.getCount(), 1, "Should have one result"); + mAsserter.is(c.getInt(c.getColumnIndex(SearchHistory.VISITS)), 2, + "Counter should be 2"); + } finally { + c.close(); + } + + cv = new ContentValues(); + cv.put(SearchHistory.QUERY, "Omaha"); + mProvider.insert(SearchHistory.CONTENT_URI, cv); + c = mProvider.query(SearchHistory.CONTENT_URI, null, null, null, null); + try { + mAsserter.is(c.getCount(), 2, "Should have two results"); + } finally { + c.close(); + } + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java new file mode 100644 index 000000000..6f82e5c51 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSearchSuggestions.java @@ -0,0 +1,115 @@ +package org.mozilla.gecko.tests; + +import java.util.ArrayList; +import java.util.HashMap; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.SuggestClient; +import org.mozilla.gecko.home.BrowserSearch; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.robotium.solo.Condition; + +/** + * Test for search suggestions. + * Sends queries from AwesomeBar input and verifies that suggestions match + * expected values. + */ +public class testSearchSuggestions extends BaseTest { + private static final int SUGGESTION_MAX = 3; + private static final int SUGGESTION_TIMEOUT = 15000; + + private static final String TEST_QUERY = "foo barz"; + private static final String SUGGESTION_TEMPLATE = "/robocop/robocop_suggestions.sjs?query=__searchTerms__"; + + public void testSearchSuggestions() { + // Mock the search system. + // The BrowserSearch UI only shows up once a non-empty + // search term is entered, but we swizzle in a new factory beforehand. + mockSuggestClientFactory(); + + blockForGeckoReady(); + + // Map of expected values. See robocop_suggestions.sjs. + final HashMap<String, ArrayList<String>> suggestMap = new HashMap<String, ArrayList<String>>(); + buildSuggestMap(suggestMap); + + focusUrlBar(); + + // At this point we rely on our swizzling having worked -- which relies + // on us not having previously run a search. + // The test will fail later if there's already a BrowserSearch object with a + // suggest client set, so fail here. + BrowserSearch browserSearch = (BrowserSearch) getBrowserSearch(); + mAsserter.ok(browserSearch == null || + browserSearch.mSuggestClient == null, + "There is no existing search client.", ""); + + // Now test the incremental suggestions. + for (int i = 0; i < TEST_QUERY.length(); i++) { + mActions.sendKeys(TEST_QUERY.substring(i, i+1)); + + final String query = TEST_QUERY.substring(0, i+1); + mSolo.waitForView(R.id.suggestion_text); + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + // Get the first suggestion row. + ViewGroup suggestionGroup = (ViewGroup) getActivity().findViewById(R.id.suggestion_layout); + if (suggestionGroup == null) { + mAsserter.dumpLog("Fail: suggestionGroup is null."); + return false; + } + + final ArrayList<String> expected = suggestMap.get(query); + for (int i = 0; i < expected.size(); i++) { + View queryChild = suggestionGroup.getChildAt(i); + if (queryChild == null || queryChild.getVisibility() == View.GONE) { + mAsserter.dumpLog("Fail: queryChild is null or GONE."); + return false; + } + + String suggestion = ((TextView) queryChild.findViewById(R.id.suggestion_text)).getText().toString(); + if (!suggestion.equals(expected.get(i))) { + mAsserter.dumpLog("Suggestion '" + suggestion + "' not equal to expected '" + expected.get(i) + "'."); + return false; + } + } + + return true; + } + }, SUGGESTION_TIMEOUT); + + mAsserter.is(success, true, "Results for query '" + query + "' matched expected suggestions"); + } + } + + private void buildSuggestMap(HashMap<String, ArrayList<String>> suggestMap) { + // these values assume SUGGESTION_MAX = 3 + suggestMap.put("f", new ArrayList<String>() {{ add("f"); add("facebook"); add("fandango"); add("frys"); }}); + suggestMap.put("fo", new ArrayList<String>() {{ add("fo"); add("forever 21"); add("food network"); add("fox news"); }}); + suggestMap.put("foo", new ArrayList<String>() {{ add("foo"); add("food network"); add("foothill college"); add("foot locker"); }}); + suggestMap.put("foo ", new ArrayList<String>() {{ add("foo "); add("foo fighters"); add("foo bar"); add("foo bat"); }}); + suggestMap.put("foo b", new ArrayList<String>() {{ add("foo b"); add("foo bar"); add("foo bat"); add("foo bay"); }}); + suggestMap.put("foo ba", new ArrayList<String>() {{ add("foo ba"); add("foo bar"); add("foo bat"); add("foo bay"); }}); + suggestMap.put("foo bar", new ArrayList<String>() {{ add("foo bar"); }}); + suggestMap.put("foo barz", new ArrayList<String>() {{ add("foo barz"); }}); + } + + private void mockSuggestClientFactory() { + BrowserSearch.sSuggestClientFactory = new BrowserSearch.SuggestClientFactory() { + @Override + public SuggestClient getSuggestClient(Context context, String template, int timeout, int max) { + final String suggestTemplate = getAbsoluteRawUrl(SUGGESTION_TEMPLATE); + + // This one uses our template, and also doesn't check for network accessibility. + return new SuggestClient(context, suggestTemplate, SUGGESTION_TIMEOUT, Integer.MAX_VALUE, false); + } + }; + } +} + diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java new file mode 100644 index 000000000..50d173461 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionHistory.java @@ -0,0 +1,37 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; + +/** + * Tests that navigating through session history (ex: forward, back) sets the correct UI state. + */ +public class testSessionHistory extends UITest { + public void testSessionHistory() { + GeckoHelper.blockForReady(); + + String url = mStringHelper.ROBOCOP_BLANK_PAGE_01_URL; + NavigationHelper.enterAndLoadUrl(url); + mToolbar.assertTitle(url); + + url = mStringHelper.ROBOCOP_BLANK_PAGE_02_URL; + NavigationHelper.enterAndLoadUrl(url); + mToolbar.assertTitle(url); + + url = mStringHelper.ROBOCOP_BLANK_PAGE_03_URL; + NavigationHelper.enterAndLoadUrl(url); + mToolbar.assertTitle(url); + + NavigationHelper.goBack(); + mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + + NavigationHelper.goBack(); + mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL); + + NavigationHelper.goForward(); + mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + + NavigationHelper.reload(); + mToolbar.assertTitle(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java new file mode 100644 index 000000000..5646311b1 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMRestore.java @@ -0,0 +1,54 @@ +package org.mozilla.gecko.tests; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; + +/** + * Tests session OOM restore behavior. + * + * Loads a session and tests that it is restored correctly. + */ +public class testSessionOOMRestore extends SessionTest { + private Session mSession; + private static final String PREFS_NAME = "GeckoApp"; + private static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle"; + + @Override + public void setActivityIntent(Intent intent) { + PageInfo home = new PageInfo(StringHelper.STATIC_ABOUT_HOME_URL); + PageInfo page1 = new PageInfo("page1"); + PageInfo page2 = new PageInfo("page2"); + PageInfo page3 = new PageInfo("page3"); + PageInfo page4 = new PageInfo("page4"); + PageInfo page5 = new PageInfo("page5"); + PageInfo page6 = new PageInfo("page6"); + + SessionTab tab1 = new SessionTab(0, home, page1, page2); + SessionTab tab2 = new SessionTab(1, home, page3, page4); + SessionTab tab3 = new SessionTab(2, home, page5, page6); + + mSession = new Session(1, tab1, tab2, tab3); + + String sessionString = buildSessionJSON(mSession); + writeProfileFile("sessionstore.js", sessionString); + + // This feature is pref-protected to prevent other apps from injecting + // a state bundle, so enable it here. + SharedPreferences prefs = getInstrumentation().getTargetContext() + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(PREFS_ALLOW_STATE_BUNDLE, true).commit(); + + Bundle bundle = new Bundle(); + bundle.putString("privateSession", null); + intent.putExtra("stateBundle", bundle); + + super.setActivityIntent(intent); + } + + public void testSessionOOMRestore() throws Exception { + blockForGeckoReady(); + verifySessionTabs(mSession); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java new file mode 100644 index 000000000..f5e5ee099 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSessionOOMSave.java @@ -0,0 +1,87 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; + +import com.robotium.solo.Condition; + +/** + * Tests session OOM save behavior. + * + * Builds a session and tests that the saved state is correct. + */ +public class testSessionOOMSave extends SessionTest { + private final static int SESSION_TIMEOUT = 25000; + + public void testSessionOOMSave() { + Actions.EventExpecter pageShowExpecter = mActions.expectGeckoEvent("Content:PageShow"); + pageShowExpecter.blockForEvent(); + pageShowExpecter.unregisterListener(); + + PageInfo home = new PageInfo(mStringHelper.ABOUT_HOME_URL); + PageInfo page1 = new PageInfo("page1"); + PageInfo page2 = new PageInfo("page2"); + PageInfo page3 = new PageInfo("page3"); + PageInfo page4 = new PageInfo("page4"); + PageInfo page5 = new PageInfo("page5"); + PageInfo page6 = new PageInfo("page6"); + + SessionTab tab1 = new SessionTab(0, home, page1, page2); + SessionTab tab2 = new SessionTab(1, home, page3, page4); + SessionTab tab3 = new SessionTab(2, home, page5, page6); + + final Session session = new Session(1, tab1, tab2, tab3); + + // Load the tabs into the browser + loadSessionTabs(session); + + // Verify sessionstore.js written by Gecko. The session write is + // delayed for certain interactions (such as changing the selected + // tab), so the file is repeatedly read until it matches the expected + // output. Because of the delay, this part of the test takes ~9 seconds + // to pass. + VerifyJSONCondition verifyJSONCondition = new VerifyJSONCondition(session); + boolean success = waitForCondition(verifyJSONCondition, SESSION_TIMEOUT); + if (success) { + mAsserter.ok(true, "verified session JSON", null); + } else { + mAsserter.ok(false, "failed to verify session JSON", + getStackTraceString(verifyJSONCondition.getLastException())); + } + } + + private class VerifyJSONCondition implements Condition { + private AssertException mLastException; + private final NonFatalAsserter mAsserter = new NonFatalAsserter(); + private final Session mSession; + + public VerifyJSONCondition(Session session) { + mSession = session; + } + + @Override + public boolean isSatisfied() { + try { + String sessionString = readProfileFile("sessionstore.js"); + if (sessionString == null) { + mLastException = new AssertException("Could not read sessionstore.js"); + return false; + } + + verifySessionJSON(mSession, sessionString, mAsserter); + } catch (AssertException e) { + mLastException = e; + return false; + } + return true; + } + + /** + * Gets the last AssertException thrown by verifySessionJSON(). + * + * This is useful to get the stack trace if the test fails. + */ + public AssertException getLastException() { + return mLastException; + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java new file mode 100644 index 000000000..0df786136 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java @@ -0,0 +1,265 @@ +package org.mozilla.gecko.tests; + +import java.util.ArrayList; +import java.util.List; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.home.HomePager; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.GridView; +import android.widget.ListView; +import android.widget.TextView; + +import com.robotium.solo.Condition; + +/** + * This test covers the opening and content of the Share Link pop-up list + * The test opens the Share menu from the app menu, the URL bar, a link context menu and the Awesomescreen tabs + */ +public class testShareLink extends AboutHomeTest { + String url; + String urlTitle = mStringHelper.ROBOCOP_BIG_LINK_TITLE; + + public void testShareLink() { + url = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL); + ArrayList<String> shareOptions; + blockForGeckoReady(); + + // FIXME: This is a temporary hack workaround for a permissions problem. + openAboutHomeTab(AboutHomeTabs.HISTORY); + + inputAndLoadUrl(url); + verifyUrlBarTitle(url); // Waiting for page title to ensure the page is loaded + + selectMenuItem(mStringHelper.SHARE_LABEL); + if (Build.VERSION.SDK_INT >= 14) { + // Check for our own sync in the submenu. + waitForText("Sync$"); + } else { + waitForText("Share via"); + } + + // Get list of current available share activities and verify them + shareOptions = getShareOptions(); + ArrayList<String> displayedOptions = getShareOptionsList(); + for (String option:shareOptions) { + // Verify if the option is present in the list of displayed share options + mAsserter.ok(optionDisplayed(option, displayedOptions), "Share option found", option); + } + + // Test share from the urlbar context menu + mSolo.goBack(); // Close the share menu + mSolo.clickLongOnText(urlTitle); + verifySharePopup(shareOptions,"urlbar"); + + // The link has a 60px height, so let's try to hit the middle + float top = mDriver.getGeckoTop() + 30 * mDevice.density; + float left = mDriver.getGeckoLeft() + mDriver.getGeckoWidth() / 2; + mSolo.clickLongOnScreen(left, top); + verifySharePopup("Share Link",shareOptions,"Link"); + + // Test the share popup in the Bookmarks page + openAboutHomeTab(AboutHomeTabs.BOOKMARKS); + + final ListView bookmarksList = findListViewWithTag(HomePager.LIST_TAG_BOOKMARKS); + mAsserter.is(waitForNonEmptyListToLoad(bookmarksList), true, "list is properly loaded"); + + int headerViewsCount = bookmarksList.getHeaderViewsCount(); + View bookmarksItem = bookmarksList.getChildAt(headerViewsCount); + if (bookmarksItem == null) { + mAsserter.dumpLog("no child at index " + headerViewsCount + "; waiting for one..."); + Condition listWaitCondition = new Condition() { + @Override + public boolean isSatisfied() { + if (bookmarksList.getChildAt(bookmarksList.getHeaderViewsCount()) == null) + return false; + return true; + } + }; + waitForCondition(listWaitCondition, MAX_WAIT_MS); + headerViewsCount = bookmarksList.getHeaderViewsCount(); + bookmarksItem = bookmarksList.getChildAt(headerViewsCount); + } + + mSolo.clickLongOnView(bookmarksItem); + verifySharePopup(shareOptions,"bookmarks"); + + // Prepopulate top sites with history items to overflow tiles. + // We are trying to move away from using reflection and doing more black-box testing. + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL)); + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL)); + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_03_URL)); + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_04_URL)); + if (mDevice.type.equals("tablet")) { + // Tablets have more tile spaces to fill. + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_05_URL)); + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_BOXES_URL)); + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_SEARCH_URL)); + inputAndLoadUrl(getAbsoluteUrl(mStringHelper.ROBOCOP_TEXT_PAGE_URL)); + } + + // Test the share popup in Top Sites. + openAboutHomeTab(AboutHomeTabs.TOP_SITES); + + // Scroll down a bit so that the top sites list has more items on screen. + int width = mDriver.getGeckoWidth(); + int height = mDriver.getGeckoHeight(); + mActions.drag(width / 2, width / 2, height - 10, height / 2); + + ListView topSitesList = findListViewWithTag(HomePager.LIST_TAG_TOP_SITES); + mAsserter.is(waitForNonEmptyListToLoad(topSitesList), true, "list is properly loaded"); + View mostVisitedItem = topSitesList.getChildAt(topSitesList.getHeaderViewsCount()); + mSolo.clickLongOnView(mostVisitedItem); + verifySharePopup(shareOptions,"top_sites"); + + // Test the share popup in the history tab + openAboutHomeTab(AboutHomeTabs.HISTORY); + + ListView mostRecentList = findListViewWithTag(HomePager.LIST_TAG_HISTORY); + mAsserter.is(waitForNonEmptyListToLoad(mostRecentList), true, "list is properly loaded"); + + // Getting second child after header views because the first is the "Today" label + View mostRecentItem = mostRecentList.getChildAt(mostRecentList.getHeaderViewsCount() + 1); + mSolo.clickLongOnView(mostRecentItem); + verifySharePopup(shareOptions,"most recent"); + } + + public void verifySharePopup(ArrayList<String> shareOptions, String openedFrom) { + verifySharePopup("Share", shareOptions, openedFrom); + } + + public void verifySharePopup(String shareItemText, ArrayList<String> shareOptions, String openedFrom) { + waitForText(shareItemText); + mSolo.clickOnText(shareItemText); + waitForText("Share via"); + ArrayList<String> displayedOptions = getSharePopupOption(); + for (String option:shareOptions) { + // Verify if the option is present in the list of displayed share options + mAsserter.ok(optionDisplayed(option, displayedOptions), "Share option for " + openedFrom + (openedFrom.equals("urlbar") ? "" : " item") + " found", option); + } + mSolo.goBack(); + /** + * Adding a wait for the page title to make sure the Awesomebar will be dismissed + * Because of Bug 712370 the Awesomescreen will be dismissed when the Share Menu is closed + * so there is no need for handling this different depending on where the share menu was invoked from + * TODO: Look more into why the delay is needed here now and it was working before + */ + waitForText(urlTitle); + } + + // Create a SEND intent and get the possible activities offered + public ArrayList<String> getShareOptions() { + ArrayList<String> shareOptions = new ArrayList<>(); + Activity currentActivity = getActivity(); + final Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_TEXT, url); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Robocop Blank 01"); + shareIntent.setType("text/plain"); + PackageManager pm = currentActivity.getPackageManager(); + List<ResolveInfo> activities = pm.queryIntentActivities(shareIntent, 0); + for (ResolveInfo activity : activities) { + shareOptions.add(activity.loadLabel(pm).toString()); + } + return shareOptions; + } + + // Traverse the group of views, adding strings from TextViews to the list. + private void getGroupTextViews(ViewGroup group, ArrayList<String> list) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child instanceof AbsListView) { + getGroupTextViews((AbsListView)child, list); + } else if (child instanceof ViewGroup) { + getGroupTextViews((ViewGroup)child, list); + } else if (child instanceof TextView) { + String viewText = ((TextView)child).getText().toString(); + if (viewText != null && viewText.length() > 0) { + list.add(viewText); + } + } + } + } + + // Traverse the group of views, adding strings from TextViews to the list. + // This override is for AbsListView, which has adapters. If adapters are + // available, it is better to use them so that child views that are not + // yet displayed can be examined. + private void getGroupTextViews(AbsListView group, ArrayList<String> list) { + for (int i = 0; i < group.getAdapter().getCount(); i++) { + View child = group.getAdapter().getView(i, null, group); + if (child instanceof AbsListView) { + getGroupTextViews((AbsListView)child, list); + } else if (child instanceof ViewGroup) { + getGroupTextViews((ViewGroup)child, list); + } else if (child instanceof TextView) { + String viewText = ((TextView)child).getText().toString(); + if (viewText != null && viewText.length() > 0) { + list.add(viewText); + } + } + } + } + + public ArrayList<String> getSharePopupOption() { + ArrayList<String> displayedOptions = new ArrayList<>(); + AbsListView shareMenu = getDisplayedShareList(); + getGroupTextViews(shareMenu, displayedOptions); + return displayedOptions; + } + + public ArrayList<String> getShareSubMenuOption() { + ArrayList<String> displayedOptions = new ArrayList<>(); + AbsListView shareMenu = getDisplayedShareList(); + getGroupTextViews(shareMenu, displayedOptions); + return displayedOptions; + } + + public ArrayList<String> getShareOptionsList() { + if (Build.VERSION.SDK_INT >= 14) { + return getShareSubMenuOption(); + } else { + return getSharePopupOption(); + } + } + + private boolean optionDisplayed(String shareOption, ArrayList<String> displayedOptions) { + for (String displayedOption: displayedOptions) { + if (shareOption.equals(displayedOption)) { + return true; + } + } + return false; + } + + private AbsListView mViewGroup; + + private AbsListView getDisplayedShareList() { + mViewGroup = null; + boolean success = waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + ArrayList<View> views = mSolo.getCurrentViews(); + for (View view : views) { + // List may be displayed in different view formats. + // On JB, GridView is common; on ICS-, ListView is common. + if (view instanceof ListView || + view instanceof GridView) { + mViewGroup = (AbsListView)view; + return true; + } + } + return false; + } + }, MAX_WAIT_MS); + mAsserter.ok(success,"Got the displayed share options?", "Got the share options view"); + return mViewGroup; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java new file mode 100644 index 000000000..893f98a51 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.util.EventCallback; +import org.mozilla.gecko.util.NativeEventListener; +import org.mozilla.gecko.util.NativeJSObject; + +public class testSnackbarAPI extends JavascriptTest implements NativeEventListener { + // Snackbar.LENGTH_INDEFINITE: To avoid tests depending on the android design support library + private static final int SNACKBAR_LENGTH_INDEFINITE = -2; + + public testSnackbarAPI() { + super("testSnackbarAPI.js"); + } + + @Override + public void handleMessage(String event, NativeJSObject message, EventCallback callback) { + mAsserter.is(event, "Snackbar:Show", "Received Snackbar:Show event"); + + try { + mAsserter.is(message.getString("message"), "This is a Snackbar", "Snackbar message"); + mAsserter.is(message.getInt("duration"), SNACKBAR_LENGTH_INDEFINITE, "Snackbar duration"); + + NativeJSObject action = message.getObject("action"); + + mAsserter.is(action.getString("label"), "Click me", "Snackbar action label"); + + } catch (Exception e) { + fFail("Event does not contain expected data: " + e.getMessage()); + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + EventDispatcher.getInstance().registerGeckoThreadListener(this, "Snackbar:Show"); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Snackbar:Show"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java new file mode 100644 index 000000000..7f7b47450 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStateWhileLoading.java @@ -0,0 +1,40 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.tests.helpers.DeviceHelper; +import org.mozilla.gecko.tests.helpers.GeckoClickHelper; +import org.mozilla.gecko.tests.helpers.GeckoHelper; +import org.mozilla.gecko.tests.helpers.NavigationHelper; +import org.mozilla.gecko.tests.helpers.WaitHelper; + +/** + * This test ensures the back/forward state is correct when switching to loading pages + * to prevent regressions like Bug 1124190. + */ +public class testStateWhileLoading extends UITest { + public void testStateWhileLoading() { + if (!DeviceHelper.isTablet()) { + // This test case only covers tablets currently. + return; + } + + GeckoHelper.blockForReady(); + + NavigationHelper.enterAndLoadUrl(mStringHelper.ROBOCOP_LINK_TO_SLOW_LOADING); + + GeckoClickHelper.openCentralizedLinkInNewTab(); + + WaitHelper.waitForPageLoad(new Runnable() { + @Override + public void run() { + mTabStrip.switchToTab(1); + + // Assert that the state of the back button is correct + // after switching to the new (still loading) tab. + mToolbar.assertBackButtonIsNotEnabled(); + } + }); + + // Assert that the state of the back button is still correct after the page has loaded. + mToolbar.assertBackButtonIsNotEnabled(); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java new file mode 100644 index 000000000..ac551b97f --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testStumblerSetting.java @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * * License, v. 2.0. If a copy of the MPL was not distributed with this + * * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.preferences.GeckoPreferences; +import org.mozilla.mozstumbler.service.AppGlobals; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; + +import com.robotium.solo.Condition; + +/* + * This test enables (checkbox checked) the Fennec setting to contribute to MLS, then waits for + * a response Intent from the stumbler service to confirm it has started. Then, it disables the + * service in the setting, and waits for confirmation that the servie has stopped. + */ +public class testStumblerSetting extends BaseTest { + boolean mIsEnabled; + + public void testStumblerSetting() { + if (!AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) { + mAsserter.info("Checking stumbler build config.", "Skipping test as Stumbler is not enabled in this build."); + return; + } + + blockForGeckoReady(); + + selectMenuItem(mStringHelper.SETTINGS_LABEL); + mAsserter.ok(mSolo.waitForText(mStringHelper.SETTINGS_LABEL), + "The Settings menu did not load", mStringHelper.SETTINGS_LABEL); + + String section = "^" + mStringHelper.MOZILLA_SECTION_LABEL + "$"; + waitForEnabledText(section); + mSolo.clickOnText(section); + + String itemTitle = "^" + mStringHelper.LOCATION_SERVICES_LABEL + "$"; + boolean foundText = waitForPreferencesText(itemTitle); + mAsserter.ok(foundText, "Waiting for settings item " + itemTitle + " in section " + section, + "The " + itemTitle + " option is present in section " + section); + + BroadcastReceiver enabledDisabledReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(AppGlobals.ACTION_TEST_SETTING_ENABLED)) { + mIsEnabled = true; + } else { + mIsEnabled = false; + } + } + }; + + Context context = getInstrumentation().getTargetContext(); + IntentFilter intentFilter = new IntentFilter(AppGlobals.ACTION_TEST_SETTING_ENABLED); + intentFilter.addAction(AppGlobals.ACTION_TEST_SETTING_DISABLED); + context.registerReceiver(enabledDisabledReceiver, intentFilter); + + boolean checked = mSolo.isCheckBoxChecked(itemTitle); + try { + mAsserter.ok(!checked, "Checking stumbler setting is unchecked.", "Unchecked as expected."); + + waitForEnabledText(itemTitle); + mSolo.clickOnText(itemTitle); + + mSolo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return mIsEnabled; + } + }, 15000); + + mAsserter.ok(mIsEnabled, "Checking if stumbler became enabled.", "Stumbler is enabled."); + mSolo.clickOnText(itemTitle); + + mSolo.waitForCondition(new Condition() { + @Override + public boolean isSatisfied() { + return !mIsEnabled; + } + }, 15000); + + mAsserter.ok(!mIsEnabled, "Checking if stumbler became disabled.", "Stumbler is disabled."); + } finally { + context.unregisterReceiver(enabledDisabledReceiver); + } + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java new file mode 100644 index 000000000..6cb42f37c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testThumbnails.java @@ -0,0 +1,116 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.db.BrowserDB; + +import android.content.ContentResolver; +import android.graphics.Color; + +import com.robotium.solo.Condition; + +/** + * Test for thumbnail updates. + * - loads 2 pages, each of which yield an HTTP 200 + * - verifies thumbnails are updated for both pages + * - loads pages again; first page yields HTTP 200, second yields HTTP 404 + * - verifies thumbnail is updated for HTTP 200, but not HTTP 404 + * - finally, test that BrowserDB.removeThumbnails drops the thumbnails + */ +public class testThumbnails extends BaseTest { + public void testThumbnails() { + final String site1Url = getAbsoluteUrl("/robocop/robocop_404.sjs?type=changeColor"); + final String site2Url = getAbsoluteUrl("/robocop/robocop_404.sjs?type=do404"); + final String site1Title = "changeColor"; + final String site2Title = "do404"; + + // the session snapshot runnable is run 500ms after document stop. a + // 3000ms delay gives us 2.5 seconds to take the screenshot, which + // should be plenty of time, even on slow devices + final int thumbnailDelay = 3000; + + blockForGeckoReady(); + + // load sites; both will return HTTP 200 with a green background + inputAndLoadUrl(site1Url); + mSolo.sleep(thumbnailDelay); + inputAndLoadUrl(site2Url); + mSolo.sleep(thumbnailDelay); + inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL); + waitForCondition(new ThumbnailTest(site1Title, Color.GREEN), 5000); + mAsserter.is(getTopSiteThumbnailColor(site1Title), Color.GREEN, "Top site thumbnail updated for HTTP 200"); + waitForCondition(new ThumbnailTest(site2Title, Color.GREEN), 5000); + mAsserter.is(getTopSiteThumbnailColor(site2Title), Color.GREEN, "Top site thumbnail updated for HTTP 200"); + + // load sites again; both will have red background, and do404 will return HTTP 404 + inputAndLoadUrl(site1Url); + mSolo.sleep(thumbnailDelay); + inputAndLoadUrl(site2Url); + mSolo.sleep(thumbnailDelay); + inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL); + waitForCondition(new ThumbnailTest(site1Title, Color.RED), 5000); + mAsserter.is(getTopSiteThumbnailColor(site1Title), Color.RED, "Top site thumbnail updated for HTTP 200"); + waitForCondition(new ThumbnailTest(site2Title, Color.GREEN), 5000); + mAsserter.is(getTopSiteThumbnailColor(site2Title), Color.GREEN, "Top site thumbnail not updated for HTTP 404"); + + // test dropping thumbnails + final ContentResolver resolver = getActivity().getContentResolver(); + final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter); + final BrowserDB db = helper.getProfileDB(); + + // check that the thumbnail is non-null + byte[] thumbnailData = db.getThumbnailForUrl(resolver, site1Url); + mAsserter.ok(thumbnailData != null && thumbnailData.length > 0, "Checking for thumbnail data", "No thumbnail data found"); + // drop thumbnails + db.removeThumbnails(resolver); + // check that the thumbnail is now null + thumbnailData = db.getThumbnailForUrl(resolver, site1Url); + mAsserter.ok(thumbnailData == null || thumbnailData.length == 0, "Checking for thumbnail data", "Thumbnail data found"); + } + + private class ThumbnailTest implements Condition { + private final String mTitle; + private final int mColor; + + public ThumbnailTest(String title, int color) { + mTitle = title; + mColor = color; + } + + @Override + public boolean isSatisfied() { + return getTopSiteThumbnailColor(mTitle) == mColor; + } + } + + private int getTopSiteThumbnailColor(String title) { + // This test is not currently run, so this just needs to compile. + return -1; +// ViewGroup topSites = (ViewGroup) getActivity().findViewById(mTopSitesId); +// if (topSites != null) { +// final int childCount = topSites.getChildCount(); +// for (int i = 0; i < childCount; i++) { +// View child = topSites.getChildAt(i); +// if (child != null) { +// TextView titleView = (TextView) child.findViewById(R.id.title); +// if (titleView != null) { +// if (titleView.getText().equals(title)) { +// ImageView thumbnailView = (ImageView) child.findViewById(R.id.thumbnail); +// if (thumbnailView != null) { +// Bitmap thumbnail = ((BitmapDrawable) thumbnailView.getDrawable()).getBitmap(); +// return thumbnail.getPixel(0, 0); +// } else { +// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find mThumbnailId: "+R.id.thumbnail); +// } +// } +// } else { +// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find R.id.title: "+R.id.title); +// } +// } else { +// mAsserter.dumpLog("getTopSiteThumbnailColor: skipped null child at index "+i); +// } +// } +// } else { +// mAsserter.dumpLog("getTopSiteThumbnailColor: unable to find mTopSitesId: " + mTopSitesId); +// } +// return -1; + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java new file mode 100644 index 000000000..c27ff0094 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testTrackingProtection.java @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail; + +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoApp; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.util.GeckoEventListener; + +import org.json.JSONException; +import org.json.JSONObject; + +public class testTrackingProtection extends JavascriptTest implements GeckoEventListener { + private String mLastTracking; + + public testTrackingProtection() { + super("testTrackingProtection.js"); + } + + @Override + public void handleMessage(String event, final JSONObject message) { + if (event.equals("Content:SecurityChange")) { + try { + JSONObject identity = message.getJSONObject("identity"); + JSONObject mode = identity.getJSONObject("mode"); + mLastTracking = mode.getString("tracking"); + mAsserter.dumpLog("Security change (tracking): " + mLastTracking); + } catch (Exception e) { + fFail("Can't extract tracking state from JSON"); + } + } + + if (event.equals("Test:Expected")) { + try { + String expected = message.getString("expected"); + mAsserter.is(mLastTracking, expected, "Tracking matched expectation"); + mAsserter.dumpLog("Testing (tracking): " + mLastTracking + " = " + expected); + } catch (Exception e) { + fFail("Can't extract expected state from JSON"); + } + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + EventDispatcher.getInstance().registerGeckoThreadListener(this, + "Content:SecurityChange", + "Test:Expected"); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, + "Content:SecurityChange", + "Test:Expected"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java new file mode 100644 index 000000000..30d7c169c --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUITelemetry.java @@ -0,0 +1,56 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.PrefsHelper; +import org.mozilla.gecko.Telemetry; +import org.mozilla.gecko.TelemetryContract.Event; +import org.mozilla.gecko.TelemetryContract.Method; +import org.mozilla.gecko.TelemetryContract.Reason; +import org.mozilla.gecko.TelemetryContract.Session; + +import android.util.Log; + +public class testUITelemetry extends JavascriptTest { + public testUITelemetry() { + super("testUITelemetry.js"); + } + + @Override + public void testJavascript() throws Exception { + blockForGeckoReady(); + + // We can't run these tests unless telemetry is turned on -- + // the events will be dropped on the floor. + Log.i("GeckoTest", "Enabling telemetry."); + PrefsHelper.setPref(AppConstants.TELEMETRY_PREF_NAME, true); + + Log.i("GeckoTest", "Adding telemetry events."); + try { + Telemetry.sendUIEvent(Event._TEST1, Method._TEST1); + Telemetry.startUISession(Session._TEST_STARTED_TWICE); + Telemetry.sendUIEvent(Event._TEST2, Method._TEST1); + + // We can only start one session per name, so this call should be ignored. + Telemetry.startUISession(Session._TEST_STARTED_TWICE); + + Telemetry.sendUIEvent(Event._TEST2, Method._TEST2); + Telemetry.startUISession(Session._TEST_STOPPED_TWICE); + Telemetry.sendUIEvent(Event._TEST3, Method._TEST1, "foobarextras"); + Telemetry.stopUISession(Session._TEST_STARTED_TWICE, Reason._TEST1); + Telemetry.sendUIEvent(Event._TEST4, Method._TEST1, "barextras"); + Telemetry.stopUISession(Session._TEST_STOPPED_TWICE, Reason._TEST2); + + // This session is already stopped, so this call should be ignored. + Telemetry.stopUISession(Session._TEST_STOPPED_TWICE, Reason._TEST_IGNORED); + + // Method defaults to Method.NONE + Telemetry.sendUIEvent(Event._TEST1); + } catch (Exception e) { + Log.e("GeckoTest", "Oops.", e); + } + + Log.i("GeckoTest", "Running remaining JS test code."); + super.testJavascript(); + } +} + diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java new file mode 100644 index 000000000..aaaded4c8 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testUnifiedTelemetryClientId.java @@ -0,0 +1,265 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import static org.mozilla.gecko.tests.helpers.AssertionHelper.*; + +import android.util.Log; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.GeckoProfile; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class testUnifiedTelemetryClientId extends JavascriptBridgeTest { + private static final String TEST_JS = "testUnifiedTelemetryClientId.js"; + + private static final String CLIENT_ID_PATH = "datareporting/state.json"; + private static final String FHR_DIR_PATH = "healthreport/"; + private static final String FHR_CLIENT_ID_PATH = FHR_DIR_PATH + "state.json"; + + private GeckoProfile profile; + private File profileDir; + private File[] filesToDeleteOnReset; + + public void setUp() throws Exception { + super.setUp(); + profile = getTestProfile(); + profileDir = profile.getDir(); // Assumes getDir is tested. + filesToDeleteOnReset = new File[] { + getClientIdFile(), + getFHRClientIdFile(), + getFHRClientIdParentDir(), + }; + } + + public void tearDown() throws Exception { + // Don't clear cache because who knows what state Gecko is in. + deleteClientIDFiles(); + super.tearDown(); + } + + private void deleteClientIDFiles() { + Log.d(LOGTAG, "deleteClientIDFiles: begin"); + + for (final File file : filesToDeleteOnReset) { + file.delete(); // can't check return value because the file may not exist before deletion. + fAssertFalse("Deleted file in reset does not exist", file.exists()); // sanity check. + } + + Log.d(LOGTAG, "deleteClientIDFiles: end"); + } + + public void testUnifiedTelemetryClientId() throws Exception { + blockForReadyAndLoadJS(TEST_JS); + fAssertTrue("Profile directory exists", profileDir.exists()); + + // Important note: we cannot stop Gecko from running while we run this test and + // Gecko is capable of creating client ID files while we run this test. However, + // ClientID.jsm will not touch modify the client ID files on disk if its client + // ID cache is filled. As such, we prevent it from touching the disk by intentionally + // priming the cache & deleting the files it added now, and resetting the cache at the + // latest possible moment before we attempt to test the client ID file. + // + // This is fragile because it relies on the ClientID cache's implementation, however, + // some alternatives (e.g. changing file system permissions, file locking) are worse + // because they can fire error handling code, which is not currently under test. + // + // First, we delete the test files - we don't want the cache prime to fail which could happen if + // these files are around & corrupted from a previous test/install. Then we prime the cache, + // and delete the files the cache priming added, so the tests are ready to add their own version + // of these files. + deleteClientIDFiles(); + primeJsClientIdCache(); + deleteClientIDFiles(); + + // TODO: If these tests weren't so expensive to run in automation, + // this should be two separate tests to avoid storing state between tests. + testJavaCreatesClientId(); // leaves cache filled. + deleteClientIDFiles(); + testJsCreatesClientId(); // leaves cache filled. + deleteClientIDFiles(); + testJavaMigratesFromHealthReport(); // leaves cache filled. + deleteClientIDFiles(); + testJsMigratesFromHealthReport(); // leaves cache filled. + + getJS().syncCall("endTest"); + } + + /** + * Scenario: Java creates client ID: + * * Fennec starts on fresh profile + * * Java code creates the client ID in datareporting/state.json + * * Js accesses client ID from the same file + * * Assert the client IDs are the same + */ + private void testJavaCreatesClientId() throws Exception { + Log.d(LOGTAG, "testJavaCreatesClientId: start"); + + fAssertFalse("Client id file does not exist yet", getClientIdFile().exists()); + + final String clientIdFromJava = getClientIdFromJava(); + resetJSCache(); + final String clientIdFromJS = getClientIdFromJS(); + // allow for the case where gecko updates the client ID after the first get + final String clientIdFromJavaAgain = getClientIdFromJava(); + fAssertTrue("Client ID from Java equals ID from JS", + clientIdFromJava.equals(clientIdFromJS) || + clientIdFromJavaAgain.equals(clientIdFromJS)); + + final String clientIdFromJSCache = getClientIdFromJS(); + resetJSCache(); + final String clientIdFromJSFileAgain = getClientIdFromJS(); + fAssertEquals("Same client ID retrieved from JS cache", clientIdFromJavaAgain, clientIdFromJSCache); + fAssertEquals("Same client ID retrieved from JS file", clientIdFromJavaAgain, clientIdFromJSFileAgain); + } + + /** + * Scenario: JS creates client ID + * * Fennec starts on a fresh profile + * * Js creates the client ID in datareporting/state.json + * * Java access the client ID from the same file + * * Assert the client IDs are the same + */ + private void testJsCreatesClientId() throws Exception { + Log.d(LOGTAG, "testJsCreatesClientId: start"); + + fAssertFalse("Client id file does not exist yet", getClientIdFile().exists()); + + resetJSCache(); + final String clientIdFromJS = getClientIdFromJS(); + final String clientIdFromJava = getClientIdFromJava(); + fAssertEquals("Client ID from JS equals ID from Java", clientIdFromJS, clientIdFromJava); + + final String clientIdFromJSCache = getClientIdFromJS(); + final String clientIdFromJavaAgain = getClientIdFromJava(); + resetJSCache(); + final String clientIdFromJSFileAgain = getClientIdFromJS(); + fAssertEquals("Same client ID retrieved from JS cache", clientIdFromJS, clientIdFromJSCache); + fAssertEquals("Same client ID retrieved from JS file", clientIdFromJS, clientIdFromJSFileAgain); + fAssertEquals("Same client ID retrieved from Java", clientIdFromJS, clientIdFromJavaAgain); + } + + /** + * Scenario: Java migrates client ID from FHR client ID file. + * * FHR file already exists. + * * Fennec starts on fresh profile + * * Java code merges client ID to datareporting/state.json from healthreport/state.json + * * Js accesses client ID from the same file + * * Assert the client IDs are the same + */ + private void testJavaMigratesFromHealthReport() throws Exception { + Log.d(LOGTAG, "testJavaMigratesFromHealthReport: start"); + + fAssertFalse("Client id file does not exist yet", getClientIdFile().exists()); + fAssertFalse("Health report file does not exist yet", getFHRClientIdFile().exists()); + + final String expectedClientId = UUID.randomUUID().toString(); + createFHRClientIdFile(expectedClientId); + + final String clientIdFromJava = getClientIdFromJava(); + fAssertEquals("Health report client ID merged by Java", expectedClientId, clientIdFromJava); + resetJSCache(); + final String clientIdFromJS = getClientIdFromJS(); + fAssertEquals("Merged client ID read by JS", expectedClientId, clientIdFromJS); + + final String clientIdFromJavaAgain = getClientIdFromJava(); + final String clientIdFromJSCache = getClientIdFromJS(); + resetJSCache(); + final String clientIdFromJSFileAgain = getClientIdFromJS(); + fAssertEquals("Same client ID retrieved from Java", expectedClientId, clientIdFromJavaAgain); + fAssertEquals("Same client ID retrieved from JS cache", expectedClientId, clientIdFromJSCache); + fAssertEquals("Same client ID retrieved from JS file", expectedClientId, clientIdFromJSFileAgain); + } + + /** + * Scenario: JS merges client ID from FHR client ID file. + * * FHR file already exists. + * * Fennec starts on a fresh profile + * * Js merges the client ID to datareporting/state.json from healthreport/state.json + * * Java access the client ID from the same file + * * Assert the client IDs are the same + */ + private void testJsMigratesFromHealthReport() throws Exception { + Log.d(LOGTAG, "testJsMigratesFromHealthReport: start"); + + fAssertFalse("Client id file does not exist yet", getClientIdFile().exists()); + fAssertFalse("Health report file does not exist yet", getFHRClientIdFile().exists()); + + final String expectedClientId = UUID.randomUUID().toString(); + createFHRClientIdFile(expectedClientId); + + resetJSCache(); + final String clientIdFromJS = getClientIdFromJS(); + fAssertEquals("Health report client ID merged by JS", expectedClientId, clientIdFromJS); + final String clientIdFromJava = getClientIdFromJava(); + fAssertEquals("Merged client ID read by Java", expectedClientId, clientIdFromJava); + + final String clientIdFromJavaAgain = getClientIdFromJava(); + final String clientIdFromJSCache = getClientIdFromJS(); + resetJSCache(); + final String clientIdFromJSFileAgain = getClientIdFromJS(); + fAssertEquals("Same client ID retrieved from Java", expectedClientId, clientIdFromJavaAgain); + fAssertEquals("Same client ID retrieved from JS cache", expectedClientId, clientIdFromJSCache); + fAssertEquals("Same client ID retrieved from JS file", expectedClientId, clientIdFromJSFileAgain); + } + + private String getClientIdFromJava() throws IOException { + // This assumes implementation details: it assumes the client ID + // file is created when Java attempts to retrieve it if it does not exist. + final String clientId = profile.getClientId(); + fAssertNotNull("Returned client ID is not null", clientId); + fAssertTrue("Client ID file exists after getClientId call", getClientIdFile().exists()); + return clientId; + } + + private String getClientIdFromJS() { + return getBlockingFromJsString("clientId"); + } + + /** + * Must be called after Gecko is loaded. + */ + private void primeJsClientIdCache() { + // Not the cleanest way, but it works. + getClientIdFromJS(); + } + + /** + * Resets the client ID cache in ClientID.jsm. This method *must* be called after + * Gecko is loaded or else this method will hang. + * + * Note: we do this for very specific reasons - see the comment in the test method + * ({@link #testUnifiedTelemetryClientId()}) for more. + */ + private void resetJSCache() { + // HACK: the backing JS method is a promise with no return value. Rather than writing a method + // to handle this (for time reasons), I call the get String method and don't access the return value. + getBlockingFromJsString("reset"); + } + + private File getClientIdFile() { + return new File(profileDir, CLIENT_ID_PATH); + } + + private File getFHRClientIdParentDir() { + return new File(profileDir, FHR_DIR_PATH); + } + + private File getFHRClientIdFile() { + return new File(profileDir, FHR_CLIENT_ID_PATH); + } + + private void createFHRClientIdFile(final String clientId) throws JSONException { + fAssertTrue("FHR directory created", getFHRClientIdParentDir().mkdirs()); + + final JSONObject obj = new JSONObject(); + obj.put("clientID", clientId); + profile.writeFile(FHR_CLIENT_ID_PATH, obj.toString()); + fAssertTrue("FHR client ID file exists after writing", getFHRClientIdFile().exists()); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java new file mode 100644 index 000000000..5164815c4 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVideoControls.java @@ -0,0 +1,9 @@ +package org.mozilla.gecko.tests; + + + +public class testVideoControls extends JavascriptTest { + public testVideoControls() { + super("testVideoControls.js"); + } +} diff --git a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java new file mode 100644 index 000000000..f5a54a0e9 --- /dev/null +++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testVkbOverlap.java @@ -0,0 +1,105 @@ +package org.mozilla.gecko.tests; + +import org.mozilla.gecko.Actions; +import org.mozilla.gecko.PaintedSurface; + +import android.net.Uri; + +/** + * A test to ensure that when an input field is focused, it is not obscured by the VKB. + * - Loads a page with an input field past the bottom of the visible area. + * - scrolls down to make the input field visible at the bottom of the screen. + * - taps on the input field to bring up the VKB + * - verifies that the input field is still visible. + */ +public class testVkbOverlap extends PixelTest { + private static final int CURSOR_BLINK_PERIOD = 500; + private static final int LESS_THAN_CURSOR_BLINK_PERIOD = CURSOR_BLINK_PERIOD - 50; + private static final int PAGE_SETTLE_TIME = 5000; + + public void testVkbOverlap() { + blockForGeckoReady(); + testSetup("initial-scale=1.0, user-scalable=no", false); + testSetup("initial-scale=1.0", false); + testSetup("", "phone".equals(mDevice.type)); + } + + private void testSetup(String viewport, boolean shouldZoom) { + loadAndPaint(getAbsoluteUrl("/robocop/test_viewport.sjs?metadata=" + Uri.encode(viewport))); + + // scroll to the bottom of the page and let it settle + Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint(); + MotionEventHelper meh = new MotionEventHelper(getInstrumentation(), mDriver.getGeckoLeft(), mDriver.getGeckoTop()); + meh.dragSync(10, 150, 10, 50); + + // the input field has a green background, so let's count the number of green pixels + int greenPixelCount = 0; + + PaintedSurface painted = waitForPaint(paintExpecter); + paintExpecter.unregisterListener(); + try { + greenPixelCount = countGreenPixels(painted); + } finally { + painted.close(); + } + + mAsserter.ok(greenPixelCount > 0, "testInputVisible", "Found " + greenPixelCount + " green pixels after scrolling"); + + paintExpecter = mActions.expectPaint(); + // the input field should be in the bottom-left corner, so tap thereabouts + meh.tap(5, mDriver.getGeckoHeight() - 5); + + // After tapping in the input field, the page needs some time to do stuff, like draw and undraw the focus highlight + // on the input field, trigger the VKB, process any resulting events generated by the system, and scroll the page. So + // we give it a few seconds to do all that. We are sufficiently generous with our definition of "few seconds" to + // prevent intermittent test failures. + try { + Thread.sleep(PAGE_SETTLE_TIME); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + + // now that the focus is in the text field we will repaint every 500ms as the cursor blinks, so we need to use a smaller + // "no paints" threshold to consider the page painted + paintExpecter.blockUntilClear(LESS_THAN_CURSOR_BLINK_PERIOD); + paintExpecter.unregisterListener(); + painted = mDriver.getPaintedSurface(); + try { + // if the vkb scrolled into view as expected, then the number of green pixels now visible should be about the + // same as it was before, since the green pixels indicate the text input is in view. use a fudge factor of 0.9 to + // account for borders and such of the text input which might still be out of view. + int newCount = countGreenPixels(painted); + + // if zooming is allowed, the number of green pixels visible should have increased substantially + if (shouldZoom) { + mAsserter.ok(newCount > greenPixelCount * 1.5, "testVkbOverlap", "Found " + newCount + " green pixels after tapping; expected " + greenPixelCount); + } else { + mAsserter.ok((Math.abs(greenPixelCount - newCount) / greenPixelCount < 0.1), "testVkbOverlap", "Found " + newCount + " green pixels after tapping; expected " + greenPixelCount); + } + } finally { + painted.close(); + } + } + + private int countGreenPixels(PaintedSurface painted) { + int count = 0; + for (int y = painted.getHeight() - 1; y >= 0; y--) { + for (int x = painted.getWidth() - 1; x >= 0; x--) { + int pixel = painted.getPixelAt(x, y); + int r = (pixel >> 16) & 0xFF; + int g = (pixel >> 8) & 0xFF; + int b = (pixel & 0xFF); + if (g > (r + 0x30) && g > (b + 0x30)) { + // there's more green in this pixel than red or blue, so count it. + // the reason this is so hacky-looking is because even though green is supposed to + // be (r,g,b) = (0x00, 0x80, 0x00), the GL readback ends up coming back quite + // different. + count++; + } + // uncomment for debugging: + // if (pixel != -1) mAsserter.dumpLog("Pixel at " + x + ", " + y + ": " + Integer.toString(pixel, 16)); + } + } + return count; + } +} |