Bug 1062016 - Editing state gets out of sync. r=yzen
authorEitan Isaacson <eitan@monotonous.org>
Tue, 09 Sep 2014 15:53:46 -0700
changeset 204435 34a6c6248521e6159831417c08c17b582f47b767
parent 204434 cc3e560c8f069710bdf1cba69c74ea363e72f595
child 204436 cf5b1e58fbb9ddd8f7232b0fa67178e8662878e6
push id27458
push usercbook@mozilla.com
push dateWed, 10 Sep 2014 12:59:53 +0000
treeherdermozilla-central@3402f4fbf7b2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen
bugs1062016
milestone35.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 1062016 - Editing state gets out of sync. r=yzen
accessible/jsat/AccessFu.jsm
accessible/jsat/EventManager.jsm
accessible/tests/mochitest/jsat/doc_content_text.html
accessible/tests/mochitest/jsat/jsatcommon.js
accessible/tests/mochitest/jsat/test_content_text.html
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -880,16 +880,17 @@ var Input = {
       let p = AccessFu.adjustContentBounds(aDetails.bounds,
                                            Utils.CurrentBrowser, true).center();
       Services.obs.notifyObservers(null, 'Gesture:LongPress',
                                    JSON.stringify({x: p.x, y: p.y}));
     }
   },
 
   setEditState: function setEditState(aEditState) {
+    Logger.debug(() => { return ['setEditState', JSON.stringify(aEditState)] });
     this.editState = aEditState;
   },
 
   // XXX: This is here for backwards compatability with screen reader simulator
   // it should be removed when the extension is updated on amo.
   scroll: function scroll(aPage, aHorizontal) {
     this.sendScrollMessage(aPage, aHorizontal);
   },
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -186,48 +186,28 @@ this.EventManager.prototype = {
       }
       case Events.SCROLLING_START:
       {
         this.contentControl.autoMove(aEvent.accessible);
         break;
       }
       case Events.TEXT_CARET_MOVED:
       {
-        let acc = aEvent.accessible;
-        let characterCount = acc.
-          QueryInterface(Ci.nsIAccessibleText).characterCount;
+        let acc = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
         let caretOffset = aEvent.
           QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
 
-        // Update editing state, both for presenter and other things
-        let state = Utils.getState(acc);
-        let editState = {
-          editing: state.contains(States.EDITABLE),
-          multiline: state.contains(States.MULTI_LINE),
-          atStart: caretOffset == 0,
-          atEnd: caretOffset == characterCount
-        };
-
-        // Not interesting
-        if (!editState.editing && editState.editing == this.editState.editing)
-          break;
-
-        if (editState.editing != this.editState.editing)
-          this.present(Presentation.editingModeChanged(editState.editing));
-
-        if (editState.editing != this.editState.editing ||
-            editState.multiline != this.editState.multiline ||
-            editState.atEnd != this.editState.atEnd ||
-            editState.atStart != this.editState.atStart)
-          this.sendMsgFunc("AccessFu:Input", editState);
-
+        // We could get a caret move in an accessible that is not focused,
+        // it doesn't mean we are not on any editable accessible. just not
+        // on this one..
+        if (Utils.getState(acc).contains(States.FOCUSED)) {
+          this._setEditingMode(aEvent, caretOffset);
+        }
         this.present(Presentation.textSelectionChanged(acc.getText(0,-1),
                      caretOffset, caretOffset, 0, 0, aEvent.isFromUserInput));
-
-        this.editState = editState;
         break;
       }
       case Events.OBJECT_ATTRIBUTE_CHANGED:
       {
         let evt = aEvent.QueryInterface(
           Ci.nsIAccessibleObjectAttributeChangedEvent);
         if (evt.changedAttribute.toString() !== 'aria-hidden') {
           // Only handle aria-hidden attribute change.
@@ -263,16 +243,17 @@ this.EventManager.prototype = {
         }
         break;
       }
       case Events.FOCUS:
       {
         // Put vc where the focus is at
         let acc = aEvent.accessible;
         let doc = aEvent.accessibleDocument;
+        this._setEditingMode(aEvent);
         if ([Roles.CHROME_WINDOW,
              Roles.DOCUMENT,
              Roles.APPLICATION].indexOf(acc.role) < 0) {
           this.contentControl.autoMove(acc);
        }
        break;
       }
       case Events.DOCUMENT_LOAD_COMPLETE:
@@ -288,16 +269,64 @@ this.EventManager.prototype = {
         if (position === target ||
             Utils.getEmbeddedControl(position) === target) {
           this.present(Presentation.valueChanged(target));
         }
       }
     }
   },
 
+  _setEditingMode: function _setEditingMode(aEvent, aCaretOffset) {
+    let acc = aEvent.accessible;
+    let accText, characterCount;
+    let caretOffset = aCaretOffset;
+
+    try {
+      accText = acc.QueryInterface(Ci.nsIAccessibleText);
+    } catch (e) {
+      // No text interface on this accessible.
+    }
+
+    if (accText) {
+      characterCount = accText.characterCount;
+      if (caretOffset === undefined) {
+        caretOffset = accText.caretOffset;
+      }
+    }
+
+    // Update editing state, both for presenter and other things
+    let state = Utils.getState(acc);
+
+    let editState = {
+      editing: state.contains(States.EDITABLE) &&
+        state.contains(States.FOCUSED),
+      multiline: state.contains(States.MULTI_LINE),
+      atStart: caretOffset === 0,
+      atEnd: caretOffset === characterCount
+    };
+
+    // Not interesting
+    if (!editState.editing && editState.editing === this.editState.editing) {
+      return;
+    }
+
+    if (editState.editing !== this.editState.editing) {
+      this.present(Presentation.editingModeChanged(editState.editing));
+    }
+
+    if (editState.editing !== this.editState.editing ||
+        editState.multiline !== this.editState.multiline ||
+        editState.atEnd !== this.editState.atEnd ||
+        editState.atStart !== this.editState.atStart) {
+      this.sendMsgFunc("AccessFu:Input", editState);
+    }
+
+    this.editState = editState;
+  },
+
   _handleShow: function _handleShow(aEvent) {
     let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
       ['additions', 'all']);
     // Only handle show if it is a relevant live region.
     if (!liveRegion) {
       return;
     }
     // Show for text is handled by the EVENT_TEXT_INSERTED handler.
--- a/accessible/tests/mochitest/jsat/doc_content_text.html
+++ b/accessible/tests/mochitest/jsat/doc_content_text.html
@@ -5,10 +5,11 @@
     <meta charset="utf-8" />
   </head>
   <body>
   <p>These are my awards, Mother. From Army.
     The seal is for marksmanship, and the gorilla is for sand racing.</p>
   <p>You're a good guy, mon frere. That means brother in French.
     I don't know how I know that. I took four years of Spanish.</p>
   <textarea>Please refrain from Mayoneggs during this salmonella scare.</textarea>
+  <label>So we don't get dessert?</label><input type="text">
   </body>
 </html>
\ No newline at end of file
--- a/accessible/tests/mochitest/jsat/jsatcommon.js
+++ b/accessible/tests/mochitest/jsat/jsatcommon.js
@@ -172,17 +172,18 @@ var AccessFuTest = {
   }
 };
 
 function AccessFuContentTest(aFuncResultPairs) {
   this.queue = aFuncResultPairs;
 }
 
 AccessFuContentTest.prototype = {
-  currentPair: null,
+  expected: [],
+  currentAction: null,
 
   start: function(aFinishedCallback) {
     Logger.logLevel = Logger.DEBUG;
     this.finishedCallback = aFinishedCallback;
     var self = this;
 
     // Get top content message manager, and set it up.
     this.mms = [Utils.getMessageManager(currentBrowser())];
@@ -231,87 +232,109 @@ AccessFuContentTest.prototype = {
           } else {
             elem.focus();
           }
         }
       });
     }
 
     aMessageManager.addMessageListener('AccessFu:Present', this);
+    aMessageManager.addMessageListener('AccessFu:Input', this);
     aMessageManager.addMessageListener('AccessFu:CursorCleared', this);
     aMessageManager.addMessageListener('AccessFu:Ready', function () {
       aMessageManager.addMessageListener('AccessFu:ContentStarted', aCallback);
       aMessageManager.sendAsyncMessage('AccessFu:Start',
         { buildApp: 'browser',
           androidSdkVersion: Utils.AndroidSdkVersion,
           logLevel: 'DEBUG' });
     });
 
     aMessageManager.loadFrameScript(
       'chrome://global/content/accessibility/content-script.js', false);
     aMessageManager.loadFrameScript(
       'data:,(' + contentScript.toString() + ')();', false);
   },
 
   pump: function() {
-    this.currentPair = this.queue.shift();
+    this.expected.shift();
+    if (this.expected.length) {
+      return;
+    }
+
+    var currentPair = this.queue.shift();
 
-    if (this.currentPair) {
-      if (typeof this.currentPair[0] === 'function') {
-        this.currentPair[0](this.mms[0]);
-      } else if (this.currentPair[0]) {
-        this.mms[0].sendAsyncMessage(this.currentPair[0].name,
-         this.currentPair[0].json);
+    if (currentPair) {
+      this.currentAction = currentPair[0];
+      if (typeof this.currentAction === 'function') {
+        this.currentAction(this.mms[0]);
+      } else if (this.currentAction) {
+        this.mms[0].sendAsyncMessage(this.currentAction.name,
+         this.currentAction.json);
       }
 
-      if (!this.currentPair[1]) {
+      this.expected = currentPair.slice(1, currentPair.length);
+
+      if (!this.expected[0]) {
        this.pump();
      }
     } else {
       this.finish();
     }
   },
 
   receiveMessage: function(aMessage) {
-    if (!this.currentPair) {
+    var expected = this.expected[0];
+
+    if (!expected) {
       return;
     }
 
-    var expected = this.currentPair[1] || {};
-
     // |expected| can simply be a name of a message, no more further testing.
     if (aMessage.name === expected) {
       ok(true, 'Received ' + expected);
       this.pump();
       return;
     }
 
-    var speech = this.extractUtterance(aMessage.json);
-    var android = this.extractAndroid(aMessage.json, expected.android);
-    if ((speech && expected.speak) || (android && expected.android)) {
+    var editState = this.extractEditeState(aMessage);
+    var speech = this.extractUtterance(aMessage);
+    var android = this.extractAndroid(aMessage, expected.android);
+    if ((speech && expected.speak)
+        || (android && expected.android)
+        || (editState && expected.editState)) {
       if (expected.speak) {
         var checkFunc = SimpleTest[expected.speak_checkFunc] || isDeeply;
         checkFunc.apply(SimpleTest, [speech, expected.speak,
           'spoken: ' + JSON.stringify(speech) +
           ' expected: ' + JSON.stringify(expected.speak) +
-          ' after: ' + (typeof this.currentPair[0] === 'function' ?
-            this.currentPair[0].toString() :
-            JSON.stringify(this.currentPair[0]))]);
+          ' after: ' + (typeof this.currentAction === 'function' ?
+            this.currentAction.toString() :
+            JSON.stringify(this.currentAction))]);
       }
 
       if (expected.android) {
         var checkFunc = SimpleTest[expected.android_checkFunc] || ok;
         checkFunc.apply(SimpleTest,
           this.lazyCompare(android, expected.android));
       }
 
+      if (expected.editState) {
+        var checkFunc = SimpleTest[expected.editState_checkFunc] || isDeeply;
+        checkFunc.apply(SimpleTest, [editState, expected.editState,
+          'editState: ' + JSON.stringify(editState) +
+          ' expected: ' + JSON.stringify(expected.editState) +
+          ' after: ' + (typeof this.currentAction === 'function' ?
+            this.currentAction.toString() :
+            JSON.stringify(this.currentAction))]);
+      }
+
       if (expected.focused) {
         var doc = currentTabDocument();
         is(doc.activeElement, doc.querySelector(expected.focused),
-          'Correct element is focused');
+          'Correct element is focused: ' + expected.focused);
       }
 
       this.pump();
     }
 
   },
 
   lazyCompare: function lazyCompare(aReceived, aExpected) {
@@ -332,38 +355,46 @@ AccessFuContentTest.prototype = {
             attr + ' [ expected ' + expected + ' got ' + received + ' ]');
           matches = false;
         }
       }
     }
     return [matches, delta.join(' ')];
   },
 
-  extractUtterance: function(aData) {
-    if (!aData) {
+  extractEditeState: function(aMessage) {
+    if (!aMessage || aMessage.name !== 'AccessFu:Input') {
       return null;
     }
 
-    for (var output of aData) {
+    return aMessage.json;
+  },
+
+  extractUtterance: function(aMessage) {
+    if (!aMessage || aMessage.name !== 'AccessFu:Present') {
+      return null;
+    }
+
+    for (var output of aMessage.json) {
       if (output && output.type === 'B2G') {
         if (output.details && output.details.data[0].string !== 'clickAction') {
           return output.details.data;
         }
       }
     }
 
     return null;
   },
 
-  extractAndroid: function(aData, aExpectedEvents) {
-    if (!aData) {
+  extractAndroid: function(aMessage, aExpectedEvents) {
+    if (!aMessage || aMessage.name !== 'AccessFu:Present') {
       return null;
     }
 
-    for (var output of aData) {
+    for (var output of aMessage.json) {
       if (output && output.type === 'Android') {
         for (var i in output.details) {
           // Only extract if event types match expected event types.
           var exp = aExpectedEvents ? aExpectedEvents[i] : null;
           if (!exp || (output.details[i].eventType !== exp.eventType)) {
             return null;
           }
         }
--- a/accessible/tests/mochitest/jsat/test_content_text.html
+++ b/accessible/tests/mochitest/jsat/test_content_text.html
@@ -127,53 +127,61 @@
               fromIndex: 0,
               toIndex: 6
             }],
             android_checkFunc: 'todo' // Bug 980512
           }],
 
           // Editable text tests.
           [ContentMessages.focusSelector('textarea'), {
-            speak: ['Please refrain from Mayoneggs during this ' +
-              'salmonella scare.', {string: 'textarea'}]
+            editState: {
+              editing: true,
+              multiline: true,
+              atStart: true,
+              atEnd: false
+            }
+           }, {
+             speak: ['Please refrain from Mayoneggs during this ' +
+               'salmonella scare.', {string: 'textarea'}]
+           }, { // When we first focus, caret is at 0.
+             android: [{
+              eventType: AndroidEvent.VIEW_TEXT_SELECTION_CHANGED,
+              brailleOutput: {
+                selectionStart: 0,
+                selectionEnd: 0
+              }
+            }]
           }],
-          [null, { // When we first focus, caret is at 0.
-              android: [{
-                eventType: AndroidEvent.VIEW_TEXT_SELECTION_CHANGED,
-                brailleOutput: {
-                  selectionStart: 0,
-                  selectionEnd: 0
-                }
-              }]
-            }
-          ],
           [ContentMessages.activateCurrent(10), {
             android: [{
               eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
               fromIndex: 0,
               toIndex: 10
             }]
-          }],
-          [null, {
+           }, {
+            editState: { editing: true,
+                         multiline: true,
+                         atStart: false,
+                         atEnd: false }
+           }, {
             android: [{
               eventType: AndroidEvent.VIEW_TEXT_SELECTION_CHANGED,
               brailleOutput: {
                 selectionStart: 10,
                 selectionEnd: 10
               }
             }]
           }],
           [ContentMessages.activateCurrent(20), {
             android: [{
               eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
               fromIndex: 10,
               toIndex: 20
             }]
-          }],
-          [null, {
+           }, {
             android: [{
               eventType: AndroidEvent.VIEW_TEXT_SELECTION_CHANGED,
               brailleOutput: {
                 selectionStart: 20,
                 selectionEnd: 20
               }
             }]
           }],
@@ -212,17 +220,76 @@
               toIndex: 59
             }]
           }],
           [ContentMessages.moveCaretPreviousBy('word'), {
             android: [{
               eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
               fromIndex: 53,
               toIndex: 59
-            }]
+            }],
+            focused: 'textarea'
+          }],
+
+          // bug xxx
+          [ContentMessages.simpleMoveNext, {
+            speak: ['So we don\'t get dessert?', {string: 'label'}],
+            focused: 'html'
+           }, {
+            editState: {
+              editing: false,
+              multiline: false,
+              atStart: true,
+              atEnd: false }
+          }],
+          [ContentMessages.simpleMoveNext, {
+            speak: [{ string : 'entry' }],
+            focused: 'html'
+          }],
+          [ContentMessages.activateCurrent(0), {
+            editState: {
+              editing: true,
+              multiline: false,
+              atStart: true,
+              atEnd: true
+            },
+            focused: 'input[type=text]'
+          }],
+          [ContentMessages.simpleMovePrevious, {
+            editState: {
+              editing: false,
+              multiline: false,
+              atStart: true,
+              atEnd: false
+            },
+            focused: 'html'
+          }],
+          [ContentMessages.simpleMoveNext, {
+            speak: [{ string : 'entry' }],
+            focused: 'html'
+          }],
+          [ContentMessages.activateCurrent(0), {
+            editState: {
+              editing: true,
+              multiline: false,
+              atStart: true,
+              atEnd: true
+            },
+            focused: 'input[type=text]'
+          }],
+          [ContentMessages.simpleMovePrevious, {
+            speak: [ 'So we don\'t get dessert?', {string: 'label'} ]
+           }, {
+            editState: {
+              editing: false,
+              multiline: false,
+              atStart: true,
+              atEnd: false
+            },
+            focused: 'html'
           }]
         ]);
 
       textTest.start(function () {
         closeBrowserWindow();
         SimpleTest.finish();
       });
     }