Bug 1067509 - Refactor jsat content test runner. r=yzen, a=test-only
authorEitan Isaacson <eitan@monotonous.org>
Mon, 22 Sep 2014 09:27:04 -0700
changeset 225248 858a86ba0a5cb6e1c6cab4df1ee6865dc3566216
parent 225247 0162625db84f004e88c5cd97195b1fc5767744fc
child 225249 2c9289c8a725a7518328a65f7b8dd21480098260
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen, test-only
bugs1067509
milestone34.0a2
Bug 1067509 - Refactor jsat content test runner. r=yzen, a=test-only
accessible/jsat/EventManager.jsm
accessible/jsat/content-script.js
accessible/tests/mochitest/jsat/jsatcommon.js
accessible/tests/mochitest/jsat/test_content_integration.html
accessible/tests/mochitest/jsat/test_content_text.html
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -39,17 +39,17 @@ this.EventManager = function EventManage
   this.sendMsgFunc = this.contentScope.sendAsyncMessage.bind(
     this.contentScope);
   this.webProgress = this.contentScope.docShell.
     QueryInterface(Ci.nsIInterfaceRequestor).
     getInterface(Ci.nsIWebProgress);
 };
 
 this.EventManager.prototype = {
-  editState: {},
+  editState: { editing: false },
 
   start: function start() {
     try {
       if (!this._started) {
         Logger.debug('EventManager.start');
 
         this._started = true;
 
@@ -193,35 +193,38 @@ this.EventManager.prototype = {
       {
         let acc = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
         let caretOffset = aEvent.
           QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
 
         // 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)) {
+        let state = Utils.getState(acc);
+        if (state.contains(States.FOCUSED)) {
           this._setEditingMode(aEvent, caretOffset);
+          if (state.contains(States.EDITABLE)) {
+            this.present(Presentation.textSelectionChanged(acc.getText(0, -1),
+              caretOffset, caretOffset, 0, 0, aEvent.isFromUserInput));
+          }
         }
-        this.present(Presentation.textSelectionChanged(acc.getText(0,-1),
-                     caretOffset, caretOffset, 0, 0, aEvent.isFromUserInput));
         break;
       }
       case Events.OBJECT_ATTRIBUTE_CHANGED:
       {
         let evt = aEvent.QueryInterface(
           Ci.nsIAccessibleObjectAttributeChangedEvent);
         if (evt.changedAttribute.toString() !== 'aria-hidden') {
           // Only handle aria-hidden attribute change.
           break;
         }
-        if (Utils.isHidden(aEvent.accessible)) {
-          this._handleHide(evt);
-        } else {
-          this._handleShow(aEvent);
+        let hidden = Utils.isHidden(aEvent.accessible);
+        this[hidden ? '_handleHide' : '_handleShow'](evt);
+        if (this.inTest) {
+          this.sendMsgFunc("AccessFu:AriaHidden", { hidden: hidden });
         }
         break;
       }
       case Events.SHOW:
       {
         this._handleShow(aEvent);
         break;
       }
@@ -249,16 +252,20 @@ this.EventManager.prototype = {
         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);
        }
+
+       if (this.inTest) {
+        this.sendMsgFunc("AccessFu:Focused");
+       }
        break;
       }
       case Events.DOCUMENT_LOAD_COMPLETE:
       {
         this.contentControl.autoMove(
           aEvent.accessible, { delay: 500 });
         break;
       }
--- a/accessible/jsat/content-script.js
+++ b/accessible/jsat/content-script.js
@@ -135,16 +135,17 @@ addMessageListener(
     if (!contentControl) {
       contentControl = new ContentControl(this);
     }
     contentControl.start();
 
     if (!eventManager) {
       eventManager = new EventManager(this, contentControl);
     }
+    eventManager.inTest = m.json.inTest;
     eventManager.start();
 
     sendAsyncMessage('AccessFu:ContentStarted');
   });
 
 addMessageListener(
   'AccessFu:Stop',
   function(m) {
--- a/accessible/tests/mochitest/jsat/jsatcommon.js
+++ b/accessible/tests/mochitest/jsat/jsatcommon.js
@@ -174,16 +174,17 @@ var AccessFuTest = {
 
 function AccessFuContentTest(aFuncResultPairs) {
   this.queue = aFuncResultPairs;
 }
 
 AccessFuContentTest.prototype = {
   expected: [],
   currentAction: null,
+  actionNum: -1,
 
   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())];
@@ -211,16 +212,23 @@ AccessFuContentTest.prototype = {
       }
     });
   },
 
   finish: function() {
     Logger.logLevel = Logger.INFO;
     for (var mm of this.mms) {
         mm.sendAsyncMessage('AccessFu:Stop');
+        mm.removeMessageListener('AccessFu:Present', this);
+        mm.removeMessageListener('AccessFu:Input', this);
+        mm.removeMessageListener('AccessFu:CursorCleared', this);
+        mm.removeMessageListener('AccessFu:Focused', this);
+        mm.removeMessageListener('AccessFu:AriaHidden', this);
+        mm.removeMessageListener('AccessFu:Ready', this);
+        mm.removeMessageListener('AccessFu:ContentStarted', this);
       }
     if (this.finishedCallback) {
       this.finishedCallback();
     }
   },
 
   setupMessageManager:  function (aMessageManager, aCallback) {
     function contentScript() {
@@ -234,22 +242,25 @@ AccessFuContentTest.prototype = {
           }
         }
       });
     }
 
     aMessageManager.addMessageListener('AccessFu:Present', this);
     aMessageManager.addMessageListener('AccessFu:Input', this);
     aMessageManager.addMessageListener('AccessFu:CursorCleared', this);
+    aMessageManager.addMessageListener('AccessFu:Focused', this);
+    aMessageManager.addMessageListener('AccessFu:AriaHidden', this);
     aMessageManager.addMessageListener('AccessFu:Ready', function () {
       aMessageManager.addMessageListener('AccessFu:ContentStarted', aCallback);
       aMessageManager.sendAsyncMessage('AccessFu:Start',
         { buildApp: 'browser',
           androidSdkVersion: Utils.AndroidSdkVersion,
-          logLevel: 'DEBUG' });
+          logLevel: 'DEBUG',
+          inTest: true });
     });
 
     aMessageManager.loadFrameScript(
       'chrome://global/content/accessibility/content-script.js', false);
     aMessageManager.loadFrameScript(
       'data:,(' + contentScript.toString() + ')();', false);
   },
 
@@ -257,16 +268,17 @@ AccessFuContentTest.prototype = {
     this.expected.shift();
     if (this.expected.length) {
       return;
     }
 
     var currentPair = this.queue.shift();
 
     if (currentPair) {
+      this.actionNum++;
       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);
       }
 
@@ -282,132 +294,28 @@ AccessFuContentTest.prototype = {
 
   receiveMessage: function(aMessage) {
     var expected = this.expected[0];
 
     if (!expected) {
       return;
     }
 
-    // |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 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.currentAction === 'function' ?
-            this.currentAction.toString() :
-            JSON.stringify(this.currentAction))]);
-      }
+    var actionsString = typeof this.currentAction === 'function' ?
+      this.currentAction.name + '()' : 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: ' + expected.focused);
-      }
-
+    if (typeof expected === 'string') {
+      ok(true, 'Got ' + expected + ' after ' + actionsString);
+      this.pump();
+    } else if (expected.ignore && !expected.ignore(aMessage)) {
+      expected.is(aMessage.json, 'after ' + actionsString +
+        ' (' + this.actionNum + ')');
+      expected.is_correct_focus();
       this.pump();
     }
-
-  },
-
-  lazyCompare: function lazyCompare(aReceived, aExpected) {
-    var matches = true;
-    var delta = [];
-    for (var attr in aExpected) {
-      var expected = aExpected[attr];
-      var received = aReceived !== undefined ? aReceived[attr] : null;
-      if (typeof expected === 'object') {
-        var [childMatches, childDelta] = this.lazyCompare(received, expected);
-        if (!childMatches) {
-          delta.push(attr + ' [ ' + childDelta + ' ]');
-          matches = false;
-        }
-      } else {
-        if (received !== expected) {
-          delta.push(
-            attr + ' [ expected ' + expected + ' got ' + received + ' ]');
-          matches = false;
-        }
-      }
-    }
-    return [matches, delta.join(' ')];
-  },
-
-  extractEditeState: function(aMessage) {
-    if (!aMessage || aMessage.name !== 'AccessFu:Input') {
-      return null;
-    }
-
-    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(aMessage, aExpectedEvents) {
-    if (!aMessage || aMessage.name !== 'AccessFu:Present') {
-      return null;
-    }
-
-    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;
-          }
-        }
-        return output.details;
-      }
-    }
-
-    return null;
   }
 };
 
 // Common content messages
 
 var ContentMessages = {
   simpleMoveFirst: {
     name: 'AccessFu:MoveCursor',
@@ -534,16 +442,223 @@ var ContentMessages = {
 
   _granularityMap: {
     'character': 1, // MOVEMENT_GRANULARITY_CHARACTER
     'word': 2, // MOVEMENT_GRANULARITY_WORD
     'paragraph': 8 // MOVEMENT_GRANULARITY_PARAGRAPH
   }
 };
 
+function ExpectedMessage (aName, aOptions) {
+  this.name = aName;
+  this.options = aOptions || {};
+  this.json = {};
+}
+
+ExpectedMessage.prototype.lazyCompare = function(aReceived, aExpected, aInfo) {
+  if (aExpected && !aReceived) {
+    return [false, 'Expected something but got nothing -- ' + aInfo];
+  }
+
+  var matches = true;
+  var delta = [];
+  for (var attr in aExpected) {
+    var expected = aExpected[attr];
+    var received = aReceived[attr];
+    if (typeof expected === 'object') {
+      var [childMatches, childDelta] = this.lazyCompare(received, expected);
+      if (!childMatches) {
+        delta.push(attr + ' [ ' + childDelta + ' ]');
+        matches = false;
+      }
+    } else {
+      if (received !== expected) {
+        delta.push(
+          attr + ' [ expected ' + JSON.stringify(expected) +
+          ' got ' + JSON.stringify(received) + ' ]');
+        matches = false;
+      }
+    }
+  }
+
+  var msg = delta.length ? delta.join(' ') : 'Structures lazily match';
+  return [matches, msg + ' -- ' + aInfo];
+};
+
+ExpectedMessage.prototype.is = function(aReceived, aInfo) {
+  var checkFunc = this.options.todo ? 'todo' : 'ok';
+  SimpleTest[checkFunc].apply(
+    SimpleTest, this.lazyCompare(aReceived, this.json, aInfo));
+};
+
+ExpectedMessage.prototype.is_correct_focus = function(aInfo) {
+  if (!this.options.focused) {
+    return;
+  }
+
+  var checkFunc = this.options.focused_todo ? 'todo_is' : 'is';
+  var doc = currentTabDocument();
+  SimpleTest[checkFunc].apply(SimpleTest,
+    [ doc.activeElement, doc.querySelector(this.options.focused),
+      'Correct element is focused: ' + this.options.focused + ' -- ' + aInfo ]);
+};
+
+ExpectedMessage.prototype.ignore = function(aMessage) {
+  return aMessage.name !== this.name;
+};
+
+function ExpectedPresent(aB2g, aAndroid, aOptions) {
+  ExpectedMessage.call(this, 'AccessFu:Present', aOptions);
+  if (aB2g) {
+    this.json.b2g = aB2g;
+  }
+
+  if (aAndroid) {
+    this.json.android = aAndroid;
+  }
+}
+
+ExpectedPresent.prototype = Object.create(ExpectedMessage.prototype);
+
+ExpectedPresent.prototype.is = function(aReceived, aInfo) {
+  var received = this.extract_presenters(aReceived);
+
+  for (var presenter of ['b2g', 'android']) {
+    if (!this.options['no_' + presenter]) {
+      var todo = this.options.todo || this.options[presenter + '_todo']
+      SimpleTest[todo ? 'todo' : 'ok'].apply(
+        SimpleTest, this.lazyCompare(received[presenter],
+          this.json[presenter], aInfo + ' (' + presenter + ')'));
+    }
+  }
+};
+
+ExpectedPresent.prototype.extract_presenters = function(aReceived) {
+  var received = { count: 0 };
+  for (var presenter of aReceived) {
+    if (presenter) {
+      received[presenter.type.toLowerCase()] = presenter.details;
+      received.count++;
+    }
+  }
+
+  return received
+};
+
+ExpectedPresent.prototype.ignore = function(aMessage) {
+  if (ExpectedMessage.prototype.ignore.call(this, aMessage)) {
+    return true;
+  }
+
+  var received = this.extract_presenters(aMessage.json);
+  return received.count === 0 ||
+    (received.visual && received.visual.eventType === 'viewport-change') ||
+    (received.android &&
+      received.android[0].eventType === AndroidEvent.VIEW_SCROLLED);
+};
+
+function ExpectedCursorChange(aSpeech, aOptions) {
+  ExpectedPresent.call(this, {
+    eventType: 'vc-change',
+    data: aSpeech
+  }, [{
+    eventType: 0x8000, // VIEW_ACCESSIBILITY_FOCUSED
+  }], aOptions);
+}
+
+ExpectedCursorChange.prototype = Object.create(ExpectedPresent.prototype);
+
+function ExpectedCursorTextChange(aSpeech, aStartOffset, aEndOffset, aOptions) {
+  ExpectedPresent.call(this, {
+    eventType: 'vc-change',
+    data: aSpeech
+  }, [{
+    eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+    fromIndex: aStartOffset,
+    toIndex: aEndOffset
+  }], aOptions);
+
+  // bug 980509
+  this.options.b2g_todo = true;
+}
+
+ExpectedCursorTextChange.prototype =
+  Object.create(ExpectedCursorChange.prototype);
+
+function ExpectedClickAction(aOptions) {
+  ExpectedPresent.call(this, {
+    eventType: 'action',
+    data: [{ string: 'clickAction' }]
+  }, [{
+    eventType: AndroidEvent.VIEW_CLICKED
+  }], aOptions);
+}
+
+ExpectedClickAction.prototype = Object.create(ExpectedPresent.prototype);
+
+function ExpectedCheckAction(aChecked, aOptions) {
+  ExpectedPresent.call(this, {
+    eventType: 'action',
+    data: [{ string: aChecked ? 'checkAction' : 'uncheckAction' }]
+  }, [{
+    eventType: AndroidEvent.VIEW_CLICKED,
+    checked: aChecked
+  }], aOptions);
+}
+
+ExpectedCheckAction.prototype = Object.create(ExpectedPresent.prototype);
+
+function ExpectedValueChange(aValue, aOptions) {
+  ExpectedPresent.call(this, {
+    eventType: 'value-change',
+    data: [aValue]
+  }, null, aOptions);
+}
+
+ExpectedValueChange.prototype = Object.create(ExpectedPresent.prototype);
+
+function ExpectedEditState(aEditState, aOptions) {
+  ExpectedMessage.call(this, 'AccessFu:Input', aOptions);
+  this.json = aEditState;
+}
+
+ExpectedEditState.prototype = Object.create(ExpectedMessage.prototype);
+
+function ExpectedTextSelectionChanged(aStart, aEnd, aOptions) {
+  ExpectedPresent.call(this, null, [{
+    eventType: AndroidEvent.VIEW_TEXT_SELECTION_CHANGED,
+    brailleOutput: {
+     selectionStart: aStart,
+     selectionEnd: aEnd
+   }}], aOptions);
+}
+
+ExpectedTextSelectionChanged.prototype =
+  Object.create(ExpectedPresent.prototype);
+
+function ExpectedTextCaretChanged(aFrom, aTo, aOptions) {
+  ExpectedPresent.call(this, null, [{
+    eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+    fromIndex: aFrom,
+    toIndex: aTo
+  }], aOptions);
+}
+
+ExpectedTextCaretChanged.prototype = Object.create(ExpectedPresent.prototype);
+
+function ExpectedAnnouncement(aAnnouncement, aOptions) {
+  ExpectedPresent.call(this, null, [{
+    eventType: AndroidEvent.ANNOUNCEMENT,
+    text: [ aAnnouncement],
+    addedCount: aAnnouncement.length
+  }], aOptions);
+}
+
+ExpectedAnnouncement.prototype = Object.create(ExpectedPresent.prototype);
+
 var AndroidEvent = {
   VIEW_CLICKED: 0x01,
   VIEW_LONG_CLICKED: 0x02,
   VIEW_SELECTED: 0x04,
   VIEW_FOCUSED: 0x08,
   VIEW_TEXT_CHANGED: 0x10,
   WINDOW_STATE_CHANGED: 0x20,
   VIEW_HOVER_ENTER: 0x80,
--- a/accessible/tests/mochitest/jsat/test_content_integration.html
+++ b/accessible/tests/mochitest/jsat/test_content_integration.html
@@ -25,255 +25,209 @@
       var doc = currentTabDocument();
       var iframe = doc.createElement('iframe');
       iframe.id = 'iframe';
       iframe.mozbrowser = true;
       iframe.addEventListener('mozbrowserloadend', function () {
       var contentTest = new AccessFuContentTest(
         [
           // Simple traversal forward
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document'],
-            focused: 'body'
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]} ,'such app'],
-            focused: 'iframe'
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['many option', {'string': 'stateNotChecked'},
-              {'string': 'checkbutton'}, {'string': 'listStart'},
-              {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}]
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(
+            ['Phone status bar', 'Traversal Rule test document'],
+            { focused: 'body' })],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
+          [ContentMessages.simpleMoveNext, new ExpectedCursorChange(
+            ['wow', {'string': 'headingLevel', 'args': [1]} ,'such app'],
+            { focused: 'iframe' })],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['many option', {'string': 'stateNotChecked'},
+            {'string': 'checkbutton'}, {'string': 'listStart'},
+            {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}])],
+
           // check checkbox
-          [ContentMessages.activateCurrent(), {
-            speak: [{'string': 'checkAction'}]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['much range', {'string': 'label'}]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['much range', '5', {'string': 'slider'}]
-          }],
-          [ContentMessages.adjustRangeUp,
-           { speak: ['6']}],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Home', {'string': 'pushbutton'}]
-          }],
+          [ContentMessages.activateCurrent(),
+           new ExpectedClickAction({ no_android: true }),
+           new ExpectedCheckAction(true, { android_todo: true })],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['much range', {'string': 'label'}])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['much range', '5', {'string': 'slider'}])],
+          [ContentMessages.adjustRangeUp, new ExpectedValueChange('6')],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
 
           // Simple traversal backward
-          [ContentMessages.simpleMovePrevious, {
-            speak: ['much range', '6', {'string': 'slider'}, 'such app']
-          }],
-          [ContentMessages.adjustRangeDown,
-           { speak: ['5']}],
-          [ContentMessages.simpleMovePrevious, {
-            speak: ['much range', {'string': 'label'}]
-          }],
-          [ContentMessages.simpleMovePrevious, {
-            speak: ['many option', {'string': 'stateChecked'},
-              {'string': 'checkbutton'}, {'string': 'listStart'},
-              {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}]
-          }],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(['much range', '6', {'string': 'slider'}, 'such app'])],
+          [ContentMessages.adjustRangeDown, new ExpectedValueChange('5')],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(['much range', {'string': 'label'}])],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(['many option', {'string': 'stateChecked'},
+            {'string': 'checkbutton'}, {'string': 'listStart'},
+            {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}])],
           // uncheck checkbox
-          [ContentMessages.activateCurrent(), {
-            speak: [{'string': 'uncheckAction'}]
-          }],
-          [ContentMessages.simpleMovePrevious, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]}]
-          }],
-          [ContentMessages.simpleMovePrevious, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
-          [ContentMessages.simpleMovePrevious, {
-            speak: ['Phone status bar']
-          }],
+          [ContentMessages.activateCurrent(),
+           new ExpectedClickAction({ no_android: true }),
+           new ExpectedCheckAction(false, { android_todo: true })],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(['wow', {'string': 'headingLevel', 'args': [1]}])],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(['Phone status bar'])],
 
-          [ContentMessages.simpleMoveNext, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
           // Moving to the absolute last item from an embedded document
           // fails. Bug 972035.
-          [ContentMessages.simpleMoveNext, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app']
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(
+            ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
           // Move from an inner frame to the last element in the parent doc
-          [ContentMessages.simpleMoveLast, {
-            speak: ['Home', {'string': 'pushbutton'}],
-            speak_checkFunc: 'todo_is'
-          }],
+          [ContentMessages.simpleMoveLast,
+            new ExpectedCursorChange(
+              ['Home', {'string': 'pushbutton'}], { b2g_todo: true })],
 
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // Moving to the absolute first item from an embedded document
           // fails. Bug 972035.
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app']
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['many option', {'string': 'stateNotChecked'},
-              {'string': 'checkbutton'}, {'string': 'listStart'},
-              {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}]
-          }],
-          [ContentMessages.simpleMoveFirst, {
-            speak: ['Phone status bar'],
-            speak_checkFunc: 'todo_is'
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
+          [ContentMessages.simpleMoveNext, new ExpectedCursorChange(
+            ['many option', {'string': 'stateNotChecked'},
+             {'string': 'checkbutton'}, {'string': 'listStart'},
+             {'string': 'list'}, {'string': 'listItemsCount', 'count': 1}])],
+          [ContentMessages.simpleMoveFirst,
+            new ExpectedCursorChange(['Phone status bar'], { b2g_todo: true })],
 
           // Reset cursors
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // Move cursor with focus in outside document
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [ContentMessages.focusSelector('button#home', false), {
-            speak: ['Home', {'string': 'pushbutton'}]
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [ContentMessages.focusSelector('button#home', false),
+           new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
 
           // Blur button and reset cursor
           [ContentMessages.focusSelector('button#home', true), null],
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // Set focus on element outside of embedded frame while
           // cursor is in frame
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app']
-          }],
-          [ContentMessages.focusSelector('button#home', false), {
-            speak: ['Home', {'string': 'pushbutton'}]
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
+          [ContentMessages.focusSelector('button#home', false),
+           new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
 
           // Blur button and reset cursor
           [ContentMessages.focusSelector('button#home', true), null],
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // XXX: Set focus on iframe itself.
           // XXX: Set focus on element in iframe when cursor is outside of it.
           // XXX: Set focus on element in iframe when cursor is in iframe.
 
           // aria-hidden element that the virtual cursor is positioned on
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
-          [doc.defaultView.ariaHideBack, {
-            speak: ["wow", {"string": "headingLevel","args": [1]}, "such app"],
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
+          [doc.defaultView.ariaHideBack,
+           new ExpectedCursorChange(
+            ["wow", {"string": "headingLevel","args": [1]}, "such app"])],
           // Changing aria-hidden attribute twice and making sure that the event
           // is fired only once when the actual change happens.
           [doc.defaultView.ariaHideBack],
           [doc.defaultView.ariaShowBack],
-          [ContentMessages.simpleMovePrevious, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // aria-hidden on the iframe that has the vc.
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app']
-          }],
-          [doc.defaultView.ariaHideIframe, {
-            speak: ['Home', {'string': 'pushbutton'}]
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
+          [doc.defaultView.ariaHideIframe,
+           new ExpectedCursorChange(['Home', {'string': 'pushbutton'}])],
           [doc.defaultView.ariaShowIframe],
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // aria-hidden element and auto Move
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
           [doc.defaultView.ariaHideBack],
-          [ContentMessages.focusSelector('button#back', false), {
+          [ContentMessages.focusSelector('button#back', false),
             // Must not speak Back button as it is aria-hidden
-            speak: ["wow", {"string": "headingLevel","args": [1]}, "such app"],
-          }],
+           new ExpectedCursorChange(
+             ["wow", {"string": "headingLevel","args": [1]}, "such app"])],
           [doc.defaultView.ariaShowBack],
           [ContentMessages.focusSelector('button#back', true), null],
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // Open dialog in outer doc, while cursor is also in outer doc
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [doc.defaultView.showAlert, {
-            speak: ['This is an alert!',
-                    {'string': 'headingLevel', 'args': [1]},
-                    {'string': 'dialog'}]
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [doc.defaultView.showAlert,
+            new ExpectedCursorChange(['This is an alert!',
+              {'string': 'headingLevel', 'args': [1]},
+              {'string': 'dialog'}])],
 
-          [doc.defaultView.hideAlert, {
-            speak: ["wow", {"string": "headingLevel","args": [1]}, "such app"],
-          }],
+          [doc.defaultView.hideAlert,
+           new ExpectedCursorChange(["wow",
+            {"string": "headingLevel", "args": [1]}, "such app"])],
 
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // Open dialog in outer doc, while cursor is in inner frame
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ["Back", {"string": "pushbutton"}]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app']
-          }],
-          [doc.defaultView.showAlert, {
-            speak: ['This is an alert!',
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(["Back", {"string": "pushbutton"}])],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(
+            ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
+          [doc.defaultView.showAlert, new ExpectedCursorChange(['This is an alert!',
                     {'string': 'headingLevel', 'args': [1]},
-                    {'string': 'dialog'}]
-          }],
+                    {'string': 'dialog'}])],
 
           // XXX: Place cursor back where it was.
-          [doc.defaultView.hideAlert, {
-            speak: ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'],
-          }],
+          [doc.defaultView.hideAlert,
+           new ExpectedCursorChange(
+            ['wow', {'string': 'headingLevel', 'args': [1]}, 'such app'])],
 
           [ContentMessages.clearCursor, 'AccessFu:CursorCleared'],
 
           // Open dialog, then focus on something when closing
-          [ContentMessages.simpleMoveNext, {
-            speak: ['Phone status bar', 'Traversal Rule test document']
-          }],
-          [doc.defaultView.showAlert, {
-            speak: ['This is an alert!',
-                    {'string': 'headingLevel', 'args': [1]},
-                    {'string': 'dialog'}]
-          }],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['Phone status bar', 'Traversal Rule test document'])],
+          [doc.defaultView.showAlert,
+           new ExpectedCursorChange(['This is an alert!',
+            {'string': 'headingLevel', 'args': [1]}, {'string': 'dialog'}])],
 
-          [function() {
+          [function hideAlertAndFocusHomeButton() {
             doc.defaultView.hideAlert();
             doc.querySelector('button#home').focus();
-          }, {
-            speak: ['Home', {'string': 'pushbutton'},
-              'Traversal Rule test document']
-          }]
+          }, new ExpectedCursorChange(['Home', {'string': 'pushbutton'},
+            'Traversal Rule test document'])]
         ]);
 
         contentTest.start(function () {
           closeBrowserWindow();
           SimpleTest.finish();
         });
 
       });
--- a/accessible/tests/mochitest/jsat/test_content_text.html
+++ b/accessible/tests/mochitest/jsat/test_content_text.html
@@ -21,276 +21,160 @@
   <script type="application/javascript" src="jsatcommon.js"></script>
 
   <script type="application/javascript">
     function doTest() {
       var doc = currentTabDocument();
       var textTest = new AccessFuContentTest(
         [
           // Read-only text tests
-          [ContentMessages.simpleMoveFirst, {
-            speak: ['These are my awards, Mother. From Army. ' +
-              'The seal is for marksmanship, and the gorilla is ' +
-              'for sand racing.', 'Text content test document']
-          }],
-          [ContentMessages.moveNextBy('word'), {
-            speak: 'These',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 0,
-              toIndex: 5
-            }]
-          }],
-          [ContentMessages.moveNextBy('word'), {
-            speak: 'are',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 6,
-              toIndex: 9
-            }]
-          }],
-          [ContentMessages.moveNextBy('word'), {
-            speak: 'my',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 10,
-              toIndex: 12
-            }]
-          }],
-          [ContentMessages.moveNextBy('word'), {
-            speak: 'awards,',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 13,
-              toIndex: 20
-            }]
-          }],
-          [ContentMessages.moveNextBy('word'), {
-            speak: 'Mother.',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 21,
-              toIndex: 28
-            }]
-          }],
-          [ContentMessages.movePreviousBy('word'), {
-            speak: 'awards,',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 13,
-              toIndex: 20
-            }]
-          }],
-          [ContentMessages.movePreviousBy('word'), {
-            speak: 'my',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 10,
-              toIndex: 12
-            }]
-          }],
-          [ContentMessages.movePreviousBy('word'), {
-            speak: 'are',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 6,
-              toIndex: 9
-            }]
-          }],
-          [ContentMessages.movePreviousBy('word'), {
-            speak: 'These',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 0,
-              toIndex: 5
-            }]
-          }],
-          [ContentMessages.simpleMoveNext, {
-            speak: 'You\'re a good guy, mon frere. ' +
+          [ContentMessages.simpleMoveFirst,
+           new ExpectedCursorChange(
+            ['These are my awards, Mother. From Army. The seal is for ' +
+             'marksmanship, and the gorilla is for sand racing.',
+             'Text content test document'])],
+          [ContentMessages.moveNextBy('word'),
+           new ExpectedCursorTextChange('These', 0, 5)],
+          [ContentMessages.moveNextBy('word'),
+           new ExpectedCursorTextChange('are', 6, 9)],
+          [ContentMessages.moveNextBy('word'),
+           new ExpectedCursorTextChange('my', 10, 12)],
+          [ContentMessages.moveNextBy('word'),
+           new ExpectedCursorTextChange('awards,', 13, 20)],
+          [ContentMessages.moveNextBy('word'),
+           new ExpectedCursorTextChange('Mother.', 21, 28)],
+          [ContentMessages.movePreviousBy('word'),
+           new ExpectedCursorTextChange('awards,', 13, 20)],
+          [ContentMessages.movePreviousBy('word'),
+           new ExpectedCursorTextChange('my', 10, 12)],
+          [ContentMessages.movePreviousBy('word'),
+           new ExpectedCursorTextChange('are', 6, 9)],
+          [ContentMessages.movePreviousBy('word'),
+           new ExpectedCursorTextChange('These', 0, 5)],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(['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.',
-          }],
+              'I took four years of Spanish.'])],
           // XXX: Word boundary should be past the apostraphe.
-          [ContentMessages.moveNextBy('word'), {
-            speak: 'You\'re',
-            speak_checkFunc: 'todo_is', // Bug 980509
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 0,
-              toIndex: 6
-            }],
-            android_checkFunc: 'todo' // Bug 980512
-          }],
+          [ContentMessages.moveNextBy('word'),
+           new ExpectedCursorTextChange('You\'re', 0, 6,
+             { android_todo: true /* Bug 980512 */})],
 
           // Editable text tests.
-          [ContentMessages.focusSelector('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
-              }
-            }]
-          }],
-          [ContentMessages.activateCurrent(10), {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 0,
-              toIndex: 10
-            }]
-           }, {
-            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
-            }]
-           }, {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_SELECTION_CHANGED,
-              brailleOutput: {
-                selectionStart: 20,
-                selectionEnd: 20
-              }
-            }]
-          }],
-          [ContentMessages.moveCaretNextBy('word'), {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 20,
-              toIndex: 29
-            }]
-          }],
-          [ContentMessages.moveCaretNextBy('word'), {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 29,
-              toIndex: 36
-            }]
-          }],
-          [ContentMessages.moveCaretNextBy('character'), {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 36,
-              toIndex: 37
-            }]
-          }],
-          [ContentMessages.moveCaretNextBy('character'), {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 37,
-              toIndex: 38
-            }]
-          }],
-          [ContentMessages.moveCaretNextBy('paragraph'), {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 38,
-              toIndex: 59
-            }]
-          }],
-          [ContentMessages.moveCaretPreviousBy('word'), {
-            android: [{
-              eventType: AndroidEvent.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-              fromIndex: 53,
-              toIndex: 59
-            }],
-            focused: 'textarea'
-          }],
+          [ContentMessages.focusSelector('textarea'),
+           new ExpectedAnnouncement('editing'),
+           new ExpectedEditState({
+            editing: true,
+            multiline: true,
+            atStart: true,
+            atEnd: false
+           }),
+           new ExpectedCursorChange(
+            ['Please refrain from Mayoneggs during this salmonella scare.',
+             {string: 'textarea'}]),
+           new ExpectedTextSelectionChanged(0, 0)
+          ],
+          [ContentMessages.activateCurrent(10),
+           new ExpectedTextCaretChanged(0, 10),
+           new ExpectedEditState({ editing: true,
+             multiline: true,
+             atStart: false,
+             atEnd: false }),
+           new ExpectedTextSelectionChanged(10, 10)],
+          [ContentMessages.activateCurrent(20),
+           new ExpectedTextCaretChanged(10, 20),
+           new ExpectedTextSelectionChanged(20, 20)
+          ],
+          [ContentMessages.moveCaretNextBy('word'),
+           new ExpectedTextCaretChanged(20, 29),
+           new ExpectedTextSelectionChanged(29, 29)
+          ],
+          [ContentMessages.moveCaretNextBy('word'),
+           new ExpectedTextCaretChanged(29, 36),
+           new ExpectedTextSelectionChanged(36, 36)
+          ],
+          [ContentMessages.moveCaretNextBy('character'),
+           new ExpectedTextCaretChanged(36, 37),
+           new ExpectedTextSelectionChanged(37, 37)
+          ],
+          [ContentMessages.moveCaretNextBy('character'),
+           new ExpectedTextCaretChanged(37, 38),
+           new ExpectedTextSelectionChanged(38, 38)
+          ],
+          [ContentMessages.moveCaretNextBy('paragraph'),
+           new ExpectedTextCaretChanged(38, 59),
+           new ExpectedTextSelectionChanged(59, 59)
+          ],
+          [ContentMessages.moveCaretPreviousBy('word'),
+           new ExpectedTextCaretChanged(53, 59),
+           new ExpectedTextSelectionChanged(53, 53)
+          ],
 
           // 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'
-          }]
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(
+            ['So we don\'t get dessert?', {string: 'label'}],
+            { focused: 'html'}),
+           new ExpectedAnnouncement('navigating'),
+           new ExpectedEditState({
+            editing: false,
+            multiline: false,
+            atStart: true,
+            atEnd: false })],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(
+            [{ string : 'entry' }],
+            { focused: 'html'})],
+          [ContentMessages.activateCurrent(0),
+           new ExpectedClickAction(),
+           new ExpectedAnnouncement('editing'),
+           new ExpectedEditState({
+            editing: true,
+            multiline: false,
+            atStart: true,
+            atEnd: true
+           }, { focused: 'input[type=text]' }),
+           new ExpectedTextSelectionChanged(0, 0)
+           ],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(
+            ['So we don\'t get dessert?', {string: 'label'}]),
+           new ExpectedAnnouncement('navigating'),
+           new ExpectedEditState({
+            editing: false,
+            multiline: false,
+            atStart: true,
+            atEnd: false
+           }, { focused: 'html' })],
+          [ContentMessages.simpleMoveNext,
+           new ExpectedCursorChange(
+            [{ string : 'entry' }],
+            { focused: 'html'})],
+          [ContentMessages.activateCurrent(0),
+           new ExpectedClickAction(),
+           new ExpectedAnnouncement('editing'),
+           new ExpectedEditState({
+            editing: true,
+            multiline: false,
+            atStart: true,
+            atEnd: true
+           },
+           { focused: 'input[type=text]' }),
+           new ExpectedTextSelectionChanged(0, 0)],
+          [ContentMessages.simpleMovePrevious,
+           new ExpectedCursorChange(
+            [ 'So we don\'t get dessert?', {string: 'label'} ]),
+           new ExpectedAnnouncement('navigating'),
+           new ExpectedEditState({
+            editing: false,
+            multiline: false,
+            atStart: true,
+            atEnd: false
+           }, { focused: 'html' })]
         ]);
 
       textTest.start(function () {
         closeBrowserWindow();
         SimpleTest.finish();
       });
     }