Bug 1067509 - Refactor jsat content test runner. r=yzen
authorEitan Isaacson <eitan@monotonous.org>
Mon, 22 Sep 2014 09:27:04 -0700
changeset 206538 a02351de6e5b954301183771b3b5e419ed891767
parent 206537 a1823a5faa109029063772aedb6e592786a97771
child 206539 82460d5ed80fe58bcaec09dfde019c6ca76ef004
push id27529
push userryanvm@gmail.com
push dateMon, 22 Sep 2014 19:49:52 +0000
treeherdermozilla-central@f4037194394e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen
bugs1067509
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 1067509 - Refactor jsat content test runner. r=yzen
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();
       });
     }