Bug 1157637 - Create ActionBar Handler and Gecko SelectionCarets tests, r=margaret
authorMark Capella <markcapella@twcny.rr.com>
Thu, 14 May 2015 22:06:13 -0400
changeset 243982 101feffdaed800475440429baace98a1be17a27a
parent 243981 1be077af56e34552a462e30ca7ce1b56ab21f786
child 243983 b0ae961951f59c840a57561894ff77d35a94d0ee
push id28761
push usercbook@mozilla.com
push dateFri, 15 May 2015 14:50:10 +0000
treeherdermozilla-central@c0e709a5baca [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs1157637
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1157637 - Create ActionBar Handler and Gecko SelectionCarets tests, r=margaret
mobile/android/base/Tab.java
mobile/android/tests/browser/robocop/robocop.ini
mobile/android/tests/browser/robocop/testSelectionCarets.html
mobile/android/tests/browser/robocop/testSelectionCarets.java
mobile/android/tests/browser/robocop/testSelectionCarets.js
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -18,16 +18,17 @@ import org.json.JSONObject;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadata;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.RemoteFavicon;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.Layer;
+import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
@@ -141,16 +142,17 @@ public class Tab {
     private ContentResolver getContentResolver() {
         return mAppContext.getContentResolver();
     }
 
     public void onDestroy() {
         Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.CLOSED);
     }
 
+    @RobocopTarget
     public int getId() {
         return mId;
     }
 
     public synchronized void onChange() {
         mLastUsed = System.currentTimeMillis();
     }
 
--- a/mobile/android/tests/browser/robocop/robocop.ini
+++ b/mobile/android/tests/browser/robocop/robocop.ini
@@ -163,16 +163,17 @@ skip-if = android_version == "10" || and
 [testJavascriptBridge]
 [testNativeCrypto]
 [testReaderModeTitle]
 [testSessionHistory]
 # disabled on Android 4.3, bug 1144879
 skip-if = android_version == "18"
 [testStateWhileLoading]
 
+[testSelectionCarets]
 # testSelectionHandler disabled on Android 2.3 by trailing skip-if, due to bug 980074
 # also disabled on Android 4.3, bug 1144882
 [testSelectionHandler]
 skip-if = android_version == "10" || android_version == "18"
 
 # testInputSelections disabled on Android 2.3 by trailing skip-if, due to bug 980074
 [testInputSelections]
 skip-if = android_version == "10"
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testSelectionCarets.html
@@ -0,0 +1,35 @@
+<html>
+  <head>
+    <title>ActionBar Handler and SelectionCarets tests</title>
+    <meta name="viewport"
+      content="initial-scale=1, allowZoom=no, maximum-scale=1,
+               user-scalable=no, width=device-width">
+  </head>
+
+  <body>
+    <div id="LTRcontenteditable"
+      style="direction: ltr; width: 10em; height: 2em; word-wrap: break-word;
+      overflow: auto; -moz-user-select:text"
+      contenteditable="true">Find my book</div>
+    <div id="RTLcontenteditable"
+      style="direction: rtl; width: 10em; height: 2em; word-wrap: break-word;
+      overflow: auto; -moz-user-select:text"
+      contenteditable="true">איפה האוטו שלי</div>
+
+    <div id="LTRtextContent"
+      style="direction: ltr; width: 10em; height: 2em; word-wrap: break-word;
+      overflow: auto; -moz-user-select:text">Open the door</div>
+    <div id="RTLtextContent"
+      style="direction: rtl; width: 10em; height: 2em; word-wrap: break-word;
+      overflow: auto; -moz-user-select:text">תן לי מים</div>
+
+    <input id="LTRinput" style="direction: ltr;" value="Type something">
+    <input id="RTLinput" style="direction: rtl;" value="לרוץ במעלה הגבעה">
+    <br>
+
+    <textarea id="LTRtextarea" style="direction: ltr;"
+      rows="3" cols="8">Words in a box</textarea>
+    <textarea id="RTLtextarea" style="direction: rtl;"
+      rows="3" cols="8">הספר הוא טוב</textarea>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testSelectionCarets.java
@@ -0,0 +1,106 @@
+/**
+ * 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.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class testSelectionCarets extends JavascriptTest implements GeckoEventListener {
+    private static final String LOGTAG = "testSelectionCarets";
+
+    private static final String LONGPRESS_EVENT = "testSelectionCarets:Longpress";
+    private static final String TAB_CHANGE_EVENT = "testSelectionCarets:TabChange";
+
+    private final TabsListener tabsListener;
+
+    public testSelectionCarets() {
+        super("testSelectionCarets.js");
+
+        tabsListener = new TabsListener();
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        Tabs.registerOnTabsChangedListener(tabsListener);
+        EventDispatcher.getInstance().registerGeckoThreadListener(this, LONGPRESS_EVENT);
+    }
+
+    @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();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        Tabs.unregisterOnTabsChangedListener(tabsListener);
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(this, LONGPRESS_EVENT);
+
+        super.tearDown();
+    }
+
+    /**
+     * The test script will request us to trigger Longpress AndroidGeckoEvents.
+    */
+    @Override
+    public void handleMessage(String event, final JSONObject message) {
+        switch(event) {
+            case LONGPRESS_EVENT: {
+                final long meTime = SystemClock.uptimeMillis();
+                final int meX = Math.round(message.optInt("x", 0));
+                final int meY = Math.round(message.optInt("y", 0));
+                final MotionEvent motionEvent =
+                    MotionEvent.obtain(meTime, meTime, MotionEvent.ACTION_DOWN, meX, meY, 0);
+
+                final GeckoEvent geckoEvent = GeckoEvent.createLongPressEvent(motionEvent);
+                GeckoAppShell.sendEventToGecko(geckoEvent);
+                break;
+            }
+        }
+    }
+
+    /**
+     * 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, Object 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;
+                    }
+                    final GeckoEvent event =
+                        GeckoEvent.createBroadcastEvent(TAB_CHANGE_EVENT, args.toString());
+                    GeckoAppShell.sendEventToGecko(event);
+                    break;
+            }
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/robocop/testSelectionCarets.js
@@ -0,0 +1,244 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import('resource://gre/modules/Geometry.jsm');
+
+const SELECTION_CARETS_PREF = "selectioncaret.enabled";
+const TOUCH_CARET_PREF = "touchcaret.enabled";
+const TEST_URL = "http://mochi.test:8888/tests/robocop/testSelectionCarets.html";
+
+// After longpress, Gecko notifys ActionBarHandler to init then update state.
+// When it does, we'll peek over its shoulder and test status.
+const LONGPRESS_EVENT = "testSelectionCarets:Longpress";
+const STATUS_UPDATE_EVENT = "ActionBar:UpdateState";
+
+// Ensures Tabs are completely loaded, viewport and zoom constraints updated, etc.
+const TAB_CHANGE_EVENT = "testSelectionCarets:TabChange";
+const TAB_STOP_EVENT = "STOP";
+
+const gChromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+
+/**
+ * Robocop test helpers.
+ */
+function ok(passed, text) {
+  do_report_result(passed, text, Components.stack.caller, false);
+}
+
+function is(lhs, rhs, text) {
+  do_report_result(lhs === rhs, "[ " + lhs + " === " + rhs + " ] " + text,
+    Components.stack.caller, false);
+}
+
+/**
+ * Wait for and return, when an expected tab change event occurs.
+ *
+ * @param tabId, The id of the target tab we're observing.
+ * @param eventType, The event type we expect.
+ * @return {Promise}
+ * @resolves The tab change object, including the matched tab id and event.
+ */
+function do_promiseTabChangeEvent(tabId, eventType) {
+  return new Promise(resolve => {
+    let observer = (subject, topic, data) => {
+      let message = JSON.parse(data);
+
+      if (message.event === eventType && message.tabId === tabId) {
+        Services.obs.removeObserver(observer, TAB_CHANGE_EVENT);
+        resolve(data);
+      }
+    }
+
+    Services.obs.addObserver(observer, TAB_CHANGE_EVENT, false);
+  });
+}
+
+/**
+ * Selection methods vary if we have an input / textarea element,
+ * or if we have basic content.
+ */
+function isInputOrTextarea(element) {
+  return ((element instanceof Ci.nsIDOMHTMLInputElement) ||
+          (element instanceof Ci.nsIDOMHTMLTextAreaElement));
+}
+
+/**
+ * Return the selection controller based on element.
+ */
+function elementSelection(element) {
+  return (isInputOrTextarea(element)) ?
+    element.editor.selection :
+    element.ownerDocument.defaultView.getSelection();
+}
+
+/**
+ * Select the first character of a target element, w/o affecting focus.
+ */
+function selectElementFirstChar(doc, element) {
+  if (isInputOrTextarea(element)) {
+    element.setSelectionRange(0, 1);
+    return;
+  }
+
+  // Simple test cases designed firstChild == #text node.
+  let range = doc.createRange();
+  range.setStart(element.firstChild, 0);
+  range.setEnd(element.firstChild, 1);
+
+  let selection = elementSelection(element);
+  selection.removeAllRanges();
+  selection.addRange(range);
+}
+
+/**
+ * Get longpress point. Determine the midpoint in the first character of
+ * the content in the element. X will be midpoint from left to right.
+ * Y will be 1/3 of the height up from the bottom to account for both
+ * LTR and smaller RTL characters. ie: |X| vs. |א|
+ */
+function getFirstCharPressPoint(doc, element, expected) {
+  // Select the first char in the element.
+  selectElementFirstChar(doc, element);
+
+  // Reality check selected char to expected.
+  let selection = elementSelection(element);
+  is(selection.toString(), expected, "Selected char should match expected char.");
+
+  // Return a point where long press should select entire word.
+  let rect = selection.getRangeAt(0).getBoundingClientRect();
+  let r = new Point(rect.left + (rect.width / 2), rect.bottom - (rect.height / 3));
+
+  return r;
+}
+
+/**
+ * Long press an element (RTL/LTR) at its calculated first character
+ * position, and return the result.
+ *
+ * @param midPoint, The screen coord for the longpress.
+ * @return {Promise}
+ * @resolves The ActionBar status, including its target focused element, and
+ *           the selected text that it sees.
+ */
+function do_promiseLongPressResult(midPoint) {
+  return new Promise(resolve => {
+    let observer = (subject, topic, data) => {
+      let ActionBarHandler = gChromeWin.ActionBarHandler;
+      if (topic === STATUS_UPDATE_EVENT) {
+        let text = ActionBarHandler._getSelectedText();
+        if (text !== "") {
+          // Remove notification observer, and resolve.
+          Services.obs.removeObserver(observer, STATUS_UPDATE_EVENT);
+          resolve({
+            focusedElement: ActionBarHandler._targetElement,
+            text: text,
+          });
+        }
+      }
+    };
+
+    // Add notification observer, trigger the longpress and wait.
+    Services.obs.addObserver(observer, STATUS_UPDATE_EVENT, false);
+    Messaging.sendRequestForResult({
+      type: LONGPRESS_EVENT,
+      x: midPoint.x,
+      y: midPoint.y,
+    });
+  });
+}
+
+/**
+ * Main test method.
+ */
+add_task(function* testSelectionCarets() {
+  // Wait to start loading our test page until after the initial browser tab is
+  // completely loaded. This allows each tab to complete its layer initialization,
+  // importantly, its viewport and zoomContraints info.
+  let BrowserApp = gChromeWin.BrowserApp;
+  yield do_promiseTabChangeEvent(BrowserApp.selectedTab.id, TAB_STOP_EVENT);
+
+  // Ensure Gecko Selection and Touch carets are enabled.
+  Services.prefs.setBoolPref(SELECTION_CARETS_PREF, true);
+  Services.prefs.setBoolPref(TOUCH_CARET_PREF, true);
+
+  // Load test page, wait for load completion, register cleanup.
+  let browser = BrowserApp.addTab(TEST_URL).browser;
+  let tab = BrowserApp.getTabForBrowser(browser);
+  yield do_promiseTabChangeEvent(tab.id, TAB_STOP_EVENT);
+
+  do_register_cleanup(function cleanup() {
+    Services.prefs.clearUserPref(SELECTION_CARETS_PREF);
+    Services.prefs.clearUserPref(TOUCH_CARET_PREF);
+    BrowserApp.closeTab(tab);
+  });
+
+  // References to test document elements.
+  let doc = browser.contentDocument;
+  let ce_LTR_elem = doc.getElementById("LTRcontenteditable");
+  let tc_LTR_elem = doc.getElementById("LTRtextContent");
+  let i_LTR_elem = doc.getElementById("LTRinput");
+  let ta_LTR_elem = doc.getElementById("LTRtextarea");
+
+  let ce_RTL_elem = doc.getElementById("RTLcontenteditable");
+  let tc_RTL_elem = doc.getElementById("RTLtextContent");
+  let i_RTL_elem = doc.getElementById("RTLinput");
+  let ta_RTL_elem = doc.getElementById("RTLtextarea");
+
+  // Locate longpress midpoints for test elements, ensure expactations.
+  let ce_LTR_midPoint = getFirstCharPressPoint(doc, ce_LTR_elem, "F");
+  let tc_LTR_midPoint = getFirstCharPressPoint(doc, tc_LTR_elem, "O");
+  let i_LTR_midPoint = getFirstCharPressPoint(doc, i_LTR_elem, "T");
+  let ta_LTR_midPoint = getFirstCharPressPoint(doc, ta_LTR_elem, "W");
+
+  let ce_RTL_midPoint = getFirstCharPressPoint(doc, ce_RTL_elem, "א");
+  let tc_RTL_midPoint = getFirstCharPressPoint(doc, tc_RTL_elem, "ת");
+  let i_RTL_midPoint = getFirstCharPressPoint(doc, i_RTL_elem, "ל");
+  let ta_RTL_midPoint = getFirstCharPressPoint(doc, ta_RTL_elem, "ה");
+
+
+  // Longpress various LTR content elements. Test focused element against
+  // expected, and selected text against expected.
+  let result = yield do_promiseLongPressResult(ce_LTR_midPoint);
+  is(result.focusedElement, ce_LTR_elem, "Focused element should match expected.");
+  is(result.text, "Find", "Selected text should match expected text.");
+
+  result = yield do_promiseLongPressResult(tc_LTR_midPoint);
+  is(result.focusedElement, null, "No focused element is expected.");
+  is(result.text, "Open", "Selected text should match expected text.");
+
+  result = yield do_promiseLongPressResult(i_LTR_midPoint);
+  is(result.focusedElement, i_LTR_elem, "Focused element should match expected.");
+  is(result.text, "Type", "Selected text should match expected text.");
+
+  result = yield do_promiseLongPressResult(ta_LTR_midPoint);
+  is(result.focusedElement, ta_LTR_elem, "Focused element should match expected.");
+  is(result.text, "Words", "Selected text should match expected text.");
+
+  // Longpress various RTL content elements. Test focused element against
+  // expected, and selected text against expected.
+  result = yield do_promiseLongPressResult(ce_RTL_midPoint);
+  is(result.focusedElement, ce_RTL_elem, "Focused element should match expected.");
+  is(result.text, "איפה", "Selected text should match expected text.");
+
+  result = yield do_promiseLongPressResult(tc_RTL_midPoint);
+  is(result.focusedElement, null, "No focused element is expected.");
+  is(result.text, "תן", "Selected text should match expected text.");
+
+  result = yield do_promiseLongPressResult(i_RTL_midPoint);
+  is(result.focusedElement, i_RTL_elem, "Focused element should match expected.");
+  is(result.text, "לרוץ", "Selected text should match expected text.");
+
+  result = yield do_promiseLongPressResult(ta_RTL_midPoint);
+  is(result.focusedElement, ta_RTL_elem, "Focused element should match expected.");
+  is(result.text, "הספר", "Selected text should match expected text.");
+
+  ok(true, "Finished all tests.");
+});
+
+run_next_test();