Merge mozilla-central to inbound. a=merge CLOSED TREE
authorBrindusan Cristian <cbrindusan@mozilla.com>
Mon, 07 Jan 2019 18:46:25 +0200
changeset 509842 0153780b0a346f828b2f235f63bf796490594493
parent 509841 072522b854f3b8e802e88606b9b5b73cc0884279 (current diff)
parent 509812 3b6cfb4b1e57165bc787d76123b036df299d368e (diff)
child 509843 b1c3821412f8f212fda67ffc7121d82d6a5d2fd4
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone66.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
Merge mozilla-central to inbound. a=merge CLOSED TREE
layout/build/nsLayoutModule.cpp
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -2123,16 +2123,20 @@ file, You can obtain one at http://mozil
       <method name="_openAutocompletePopup">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body><![CDATA[
           if (this.mPopupOpen) {
             return;
           }
 
+          // Explicitly set the direction of the popup because automplete.xml
+          // expects this.
+          this.style.direction = (RTL_UI ? "rtl" : "ltr");
+
           // Make the popup span the width of the window.  First, set its width.
           let documentRect =
             window.windowUtils
                 .getBoundsWithoutFlushing(window.document.documentElement);
           let width = documentRect.right - documentRect.left;
           this.setAttribute("width", width);
 
           // Now make its starting margin negative so that its leading edge
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -112,16 +112,18 @@ function triggerAutofillAndCheckProfile(
   for (const [fieldName, value] of Object.entries(adaptedProfile)) {
     const element = document.getElementById(fieldName);
     const expectingEvent = document.activeElement == element ? "DOMAutoComplete" : "change";
     const checkFieldAutofilled = Promise.all([
       new Promise(resolve => element.addEventListener("input", (event) => {
         if (element.tagName == "INPUT" && element.type == "text") {
           ok(event instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface on ${element.tagName}`);
+          is(event.inputType, "insertReplacementText",
+             "inputType value should be \"insertReplacementText\"");
         } else {
           ok(event instanceof Event && !(event instanceof UIEvent),
              `"input" event should be dispatched with Event interface on ${element.tagName}`);
         }
         is(event.cancelable, false,
            `"input" event should be never cancelable on ${element.tagName}`);
         is(event.bubbles, true,
            `"input" event should always bubble on ${element.tagName}`);
--- a/browser/extensions/formautofill/test/mochitest/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_clear_form.html
@@ -69,16 +69,18 @@ async function confirmClear(selector) {
   let promise = new Promise(resolve =>
     document.querySelector(selector).addEventListener("input", (event) => {
       ok(event instanceof InputEvent,
          '"input" event should be dispatched with InputEvent interface');
       is(event.cancelable, false,
          '"input" event should be never cancelable');
       is(event.bubbles, true,
          '"input" event should always bubble');
+      is(event.inputType, "insertReplacementText",
+         'inputType value should be "insertReplacementText"');
       resolve();
     }, {once: true})
   );
   synthesizeKey("KEY_Enter");
   await promise;
 }
 
 add_task(async function simple_clear() {
--- a/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_multi_locale_CA_address_form.html
@@ -47,16 +47,18 @@ let MOCK_STORAGE = [{
 function checkElementFilled(element, expectedvalue) {
   return [
     new Promise(resolve => {
       element.addEventListener("input", function onInput(event) {
         ok(true, "Checking " + element.name + " field fires input event");
         if (element.tagName == "INPUT" && element.type == "text") {
           ok(event instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface on ${element.name}`);
+          is(event.inputType, "insertReplacementText",
+             "inputType value should be \"insertReplacementText\"");
         } else {
           ok(event instanceof Event && !(event instanceof UIEvent),
              `"input" event should be dispatched with Event interface on ${element.name}`);
         }
         is(event.cancelable, false,
            `"input" event should be never cancelable on ${element.name}`);
         is(event.bubbles, true,
            `"input" event should always bubble on ${element.name}`);
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -64,27 +64,28 @@
 .customizationmode-checkbox,
 .customizationmode-button {
   margin: 6px 10px;
   padding: 2px 5px;
 }
 
 .customizationmode-checkbox:not(:-moz-lwtheme),
 .customizationmode-button {
-  color: rgb(71, 71, 71);
+  /* !important overrides :hover:active color from button.css on Mac */
+  color: rgb(71, 71, 71) !important;
 }
 
 #customization-reset-button,
 #customization-undo-reset-button,
 #customization-done-button {
   min-width: 10em;
 }
 
 #customization-done-button {
-  color: #fff;
+  color: #fff !important;
   font-weight: 700;
   border-color: #0060df;
   background-color: #0a84ff;
 }
 
 .customizationmode-button > .box-inherit {
   border-width: 0;
   padding: 3px 0;
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -52,17 +52,16 @@
 
 .downloadsPanelFooterButton:hover {
   outline: 1px solid var(--arrowpanel-dimmed);
 }
 
 .downloadsPanelFooterButton:hover:active,
 .downloadsPanelFooterButton[open="true"] {
   outline: 1px solid var(--arrowpanel-dimmed-further);
-  box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
 }
 
 .downloadsPanelFooterButton > .button-box {
   padding: 0;
 }
 
 #downloadsSummary {
   /* Reserve the same space as the button and separator in download items. */
@@ -172,17 +171,17 @@
 
 .downloadButton {
   -moz-appearance: none;
   min-width: 58px;
   margin: 0;
   border: none;
   background: transparent;
   padding: 0;
-  color: inherit;
+  color: inherit !important /* !important overrides button.css on Mac and Linux */;
 }
 
 .downloadButton > .button-box > .button-icon {
   width: 16px;
   height: 16px;
   margin: 1px;
   -moz-context-properties: fill;
   fill: currentColor;
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -591,17 +591,21 @@ function createHighlightButton(highlight
       // Starting with FF63, higlighter's spec accept a null first argument.
       // Still pass an empty object to fake a domnode front in order to support old
       // servers.
       return highlighter.show({});
     },
     isChecked(toolbox) {
       // if the inspector doesn't exist, then the highlighter has not yet been connected
       // to the front end.
-      const inspectorFront = toolbox.target.getCachedFront("inspector");
+      // TODO: we are using target._inspector here, but we should be using
+      // target.getCachedFront. This is a temporary solution until the inspector no
+      // longer relies on the toolbox and can be destroyed the same way any other
+      // front would be. Related: #1487677
+      const inspectorFront = toolbox.target._inspector;
       if (!inspectorFront) {
         // initialize the inspector front asyncronously. There is a potential for buggy
         // behavior here, but we need to change how the buttons get data (have them
         // consume data from reducers rather than writing our own version) in order to
         // fix this properly.
         return false;
       }
       const highlighter = inspectorFront.getKnownHighlighter(highlighterName);
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -194,72 +194,71 @@ const TargetFactory = exports.TargetFact
  *                  The DebuggerClient instance to be used to debug this target.
  * @param {Boolean} chrome
  *                  True, if we allow to see privileged resources like JSM, xpcom,
  *                  frame scripts...
  * @param {xul:tab} tab (optional)
  *                  If the target is a local Firefox tab, a reference to the firefox
  *                  frontend tab object.
  */
-function Target({ activeTab, client, chrome, tab = null }) {
-  EventEmitter.decorate(this);
-  this.destroy = this.destroy.bind(this);
-  this._onTabNavigated = this._onTabNavigated.bind(this);
-  this.activeConsole = null;
+class Target extends EventEmitter {
+  constructor({ client, chrome, activeTab, tab = null }) {
+    if (!activeTab) {
+      throw new Error("Cannot instanciate target without a non-null activeTab");
+    }
+
+    super();
 
-  if (!activeTab) {
-    throw new Error("Cannot instanciate target without a non-null activeTab");
-  }
-  this.activeTab = activeTab;
+    this.destroy = this.destroy.bind(this);
+    this._onTabNavigated = this._onTabNavigated.bind(this);
+    this.activeConsole = null;
+    this.activeTab = activeTab;
+
+    this._url = this.form.url;
+    this._title = this.form.title;
+
+    this._client = client;
+    this._chrome = chrome;
 
-  this._url = this.form.url;
-  this._title = this.form.title;
-
-  this._client = client;
-  this._chrome = chrome;
+    // When debugging local tabs, we also have a reference to the Firefox tab
+    // This is used to:
+    // * distinguish local tabs from remote (see target.isLocalTab)
+    // * being able to hookup into Firefox UI (see Hosts)
+    if (tab) {
+      this._tab = tab;
+      this._setupListeners();
+    }
 
-  // When debugging local tabs, we also have a reference to the Firefox tab
-  // This is used to:
-  // * distinguish local tabs from remote (see target.isLocalTab)
-  // * being able to hookup into Firefox UI (see Hosts)
-  if (tab) {
-    this._tab = tab;
-    this._setupListeners();
+    // isBrowsingContext is true for all target connected to an actor that inherits from
+    // BrowsingContextTargetActor. It happens to be the case for almost all targets but:
+    // * legacy add-ons (old bootstrapped add-ons)
+    // * content process (browser content toolbox)
+    // * xpcshell debugging (it uses ParentProcessTargetActor, which inherits from
+    //                       BrowsingContextActor, but doesn't have any valid browsing
+    //                       context to attach to.)
+    // Starting with FF64, BrowsingContextTargetActor exposes a traits to help identify
+    // the target actors inheriting from it. It also help identify the xpcshell debugging
+    // target actor that doesn't have any valid browsing context.
+    // (Once FF63 is no longer supported, we can remove the `else` branch and only look
+    // for the traits)
+    if (this.form.traits && ("isBrowsingContext" in this.form.traits)) {
+      this._isBrowsingContext = this.form.traits.isBrowsingContext;
+    } else {
+      this._isBrowsingContext = !this.isLegacyAddon && !this.isContentProcess && !this.isWorkerTarget;
+    }
+
+    // Cache of already created targed-scoped fronts
+    // [typeName:string => Front instance]
+    this.fronts = new Map();
+    // Temporary fix for bug #1493131 - inspector has a different life cycle
+    // than most other fronts because it is closely related to the toolbox.
+    // TODO: remove once inspector is separated from the toolbox
+    this._inspector = null;
   }
 
-  // isBrowsingContext is true for all target connected to an actor that inherits from
-  // BrowsingContextTargetActor. It happens to be the case for almost all targets but:
-  // * legacy add-ons (old bootstrapped add-ons)
-  // * content process (browser content toolbox)
-  // * xpcshell debugging (it uses ParentProcessTargetActor, which inherits from
-  //                       BrowsingContextActor, but doesn't have any valid browsing
-  //                       context to attach to.)
-  // Starting with FF64, BrowsingContextTargetActor exposes a traits to help identify
-  // the target actors inheriting from it. It also help identify the xpcshell debugging
-  // target actor that doesn't have any valid browsing context.
-  // (Once FF63 is no longer supported, we can remove the `else` branch and only look
-  // for the traits)
-  if (this.form.traits && ("isBrowsingContext" in this.form.traits)) {
-    this._isBrowsingContext = this.form.traits.isBrowsingContext;
-  } else {
-    this._isBrowsingContext = !this.isLegacyAddon && !this.isContentProcess && !this.isWorkerTarget;
-  }
-
-  // Cache of already created targed-scoped fronts
-  // [typeName:string => Front instance]
-  this.fronts = new Map();
-  // Temporary fix for bug #1493131 - inspector has a different life cycle
-  // than most other fronts because it is closely related to the toolbox.
-  // TODO: remove once inspector is separated from the toolbox
-  this._inspector = null;
-}
-
-exports.Target = Target;
-
-Target.prototype = {
   /**
    * Returns a promise for the protocol description from the root actor. Used
    * internally with `target.actorHasMethod`. Takes advantage of caching if
    * definition was fetched previously with the corresponding actor information.
    * Actors are lazily loaded, so not only must the tool using a specific actor
    * be in use, the actors are only registered after invoking a method (for
    * performance reasons, added in bug 988237), so to use these actor detection
    * methods, one must already be communicating with a specific actor of that
@@ -286,215 +285,215 @@ Target.prototype = {
    *       "substring": {
    *         "_retval": "primitive"
    *       }
    *     }
    *   }],
    *  "events": {}
    * }
    */
-  getActorDescription: async function(actorName) {
+  async getActorDescription(actorName) {
     if (this._protocolDescription &&
         this._protocolDescription.types[actorName]) {
       return this._protocolDescription.types[actorName];
     }
     const description = await this.client.mainRoot.protocolDescription();
     this._protocolDescription = description;
     return description.types[actorName];
-  },
+  }
 
   /**
    * Returns a boolean indicating whether or not the specific actor
    * type exists.
    *
    * @param {String} actorName
    * @return {Boolean}
    */
-  hasActor: function(actorName) {
+  hasActor(actorName) {
     if (this.form) {
       return !!this.form[actorName + "Actor"];
     }
     return false;
-  },
+  }
 
   /**
    * Queries the protocol description to see if an actor has
    * an available method. The actor must already be lazily-loaded (read
    * the restrictions in the `getActorDescription` comments),
    * so this is for use inside of tool. Returns a promise that
    * resolves to a boolean.
    *
    * @param {String} actorName
    * @param {String} methodName
    * @return {Promise}
    */
-  actorHasMethod: function(actorName, methodName) {
+  actorHasMethod(actorName, methodName) {
     return this.getActorDescription(actorName).then(desc => {
       if (desc && desc.methods) {
         return !!desc.methods.find(method => method.name === methodName);
       }
       return false;
     });
-  },
+  }
 
   /**
    * Returns a trait from the root actor.
    *
    * @param {String} traitName
    * @return {Mixed}
    */
-  getTrait: function(traitName) {
+  getTrait(traitName) {
     // If the targeted actor exposes traits and has a defined value for this
     // traits, override the root actor traits
     if (this.form.traits && traitName in this.form.traits) {
       return this.form.traits[traitName];
     }
 
     return this.client.traits[traitName];
-  },
+  }
 
   get tab() {
     return this._tab;
-  },
+  }
 
   get form() {
     return this.activeTab.targetForm;
-  },
+  }
 
   // Get a promise of the RootActor's form
   get root() {
     return this.client.mainRoot.rootForm;
-  },
+  }
 
   // Temporary fix for bug #1493131 - inspector has a different life cycle
   // than most other fronts because it is closely related to the toolbox.
   // TODO: remove once inspector is separated from the toolbox
   async getInspector(typeName) {
     // the front might have been destroyed and no longer have an actor ID
     if (this._inspector && this._inspector.actorID) {
       return this._inspector;
     }
     this._inspector = await getFront(this.client, "inspector", this.form);
     this.emit("inspector", this._inspector);
     return this._inspector;
-  },
+  }
 
   // Run callback on every front of this type that currently exists, and on every
   // instantiation of front type in the future.
   onFront(typeName, callback) {
     const front = this.fronts.get(typeName);
     if (front) {
       return callback(front);
     }
     return this.on(typeName, callback);
-  },
+  }
 
   // Get a Front for a target-scoped actor.
   // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
   async getFront(typeName) {
     let front = this.fronts.get(typeName);
     // the front might have been destroyed and no longer have an actor ID
     if (front && front.actorID || front && typeof front.then === "function") {
       return front;
     }
     front = getFront(this.client, typeName, this.form);
     this.fronts.set(typeName, front);
     // replace the placeholder with the instance of the front once it has loaded
     front = await front;
     this.emit(typeName, front);
     this.fronts.set(typeName, front);
     return front;
-  },
+  }
 
   getCachedFront(typeName) {
     // do not wait for async fronts;
     const front = this.fronts.get(typeName);
     // ensure that the front is a front, and not async front
     if (front && front.actorID) {
       return front;
     }
     return null;
-  },
+  }
 
   get client() {
     return this._client;
-  },
+  }
 
   // Tells us if we are debugging content document
   // or if we are debugging chrome stuff.
   // Allows to controls which features are available against
   // a chrome or a content document.
   get chrome() {
     return this._chrome;
-  },
+  }
 
   // Tells us if the related actor implements BrowsingContextTargetActor
   // interface and requires to call `attach` request before being used and
   // `detach` during cleanup.
   get isBrowsingContext() {
     return this._isBrowsingContext;
-  },
+  }
 
   get name() {
     if (this.isAddon) {
       return this.form.name;
     }
     return this._title;
-  },
+  }
 
   get url() {
     return this._url;
-  },
+  }
 
   get isAddon() {
     return this.isLegacyAddon || this.isWebExtension;
-  },
+  }
 
   get isWorkerTarget() {
     return this.activeTab && this.activeTab.typeName === "workerTarget";
-  },
+  }
 
   get isLegacyAddon() {
     return !!(this.form && this.form.actor &&
       this.form.actor.match(/conn\d+\.addon(Target)?\d+/));
-  },
+  }
 
   get isWebExtension() {
     return !!(this.form && this.form.actor && (
       this.form.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
       this.form.actor.match(/child\d+\/webExtension(Target)?\d+/)
     ));
-  },
+  }
 
   get isContentProcess() {
     // browser content toolbox's form will be of the form:
     //   server0.conn0.content-process0/contentProcessTarget7
     // while xpcshell debugging will be:
     //   server1.conn0.contentProcessTarget7
     return !!(this.form && this.form.actor &&
       this.form.actor.match(/conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/));
-  },
+  }
 
   get isLocalTab() {
     return !!this._tab;
-  },
+  }
 
   get isMultiProcess() {
     return !this.window;
-  },
+  }
 
   get canRewind() {
     return this.activeTab && this.activeTab.traits.canRewind;
-  },
+  }
 
   isReplayEnabled() {
     return Services.prefs.getBoolPref("devtools.recordreplay.mvp.enabled")
       && this.canRewind
       && this.isLocalTab;
-  },
+  }
 
   getExtensionPathName(url) {
     // Return the url if the target is not a webextension.
     if (!this.isWebExtension) {
       throw new Error("Target is not a WebExtension");
     }
 
     try {
@@ -503,30 +502,30 @@ Target.prototype = {
       if (parsedURL.protocol !== "moz-extension:") {
         return url;
       }
       return parsedURL.pathname;
     } catch (e) {
       // Return the url if unable to resolve the pathname.
       return url;
     }
-  },
+  }
 
   /**
    * For local tabs, returns the tab's contentPrincipal, which can be used as a
    * `triggeringPrincipal` when opening links.  However, this is a hack as it is not
    * correct for subdocuments and it won't work for remote debugging.  Bug 1467945 hopes
    * to devise a better approach.
    */
   get contentPrincipal() {
     if (!this.isLocalTab) {
       return null;
     }
     return this.tab.linkedBrowser.contentPrincipal;
-  },
+  }
 
   /**
    * Attach the target and its console actor.
    *
    * This method will mainly call `attach` request on the target actor as well
    * as the console actor.
    * See DebuggerClient.attachTarget and DebuggerClient.attachConsole for more info.
    * It also starts listenings to events the target actor will start emitting
@@ -609,42 +608,42 @@ Target.prototype = {
       // as it depends on `activeTab` which is set by this method.
       this._setupRemoteListeners();
 
       // But all target actor have a console actor to attach
       return attachConsole();
     })();
 
     return this._attach;
-  },
+  }
 
   /**
    * Listen to the different events.
    */
-  _setupListeners: function() {
+  _setupListeners() {
     this.tab.addEventListener("TabClose", this);
     this.tab.ownerDocument.defaultView.addEventListener("unload", this);
     this.tab.addEventListener("TabRemotenessChange", this);
-  },
+  }
 
   /**
    * Teardown event listeners.
    */
-  _teardownListeners: function() {
+  _teardownListeners() {
     if (this._tab.ownerDocument.defaultView) {
       this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
     }
     this._tab.removeEventListener("TabClose", this);
     this._tab.removeEventListener("TabRemotenessChange", this);
-  },
+  }
 
   /**
    * Event listener for tabNavigated packet sent by activeTab's front.
    */
-  _onTabNavigated: function(packet) {
+  _onTabNavigated(packet) {
     const event = Object.create(null);
     event.url = packet.url;
     event.title = packet.title;
     event.nativeConsoleAPI = packet.nativeConsoleAPI;
     event.isFrameSwitching = packet.isFrameSwitching;
 
     // Keep the title unmodified when a developer toolbox switches frame
     // for a tab (Bug 1261687), but always update the title when the target
@@ -661,22 +660,22 @@ Target.prototype = {
       event._navPayload = this._navRequest;
       this.emit("will-navigate", event);
       this._navRequest = null;
     } else {
       event._navPayload = this._navWindow;
       this.emit("navigate", event);
       this._navWindow = null;
     }
-  },
+  }
 
   /**
    * Setup listeners for remote debugging, updating existing ones as necessary.
    */
-  _setupRemoteListeners: function() {
+  _setupRemoteListeners() {
     this.client.addListener("closed", this.destroy);
 
     // For now, only browsing-context inherited actors are using a front,
     // for which events have to be listened on the front itself.
     // For other actors (ContentProcessTargetActor and AddonTargetActor), events should
     // still be listened directly on the client. This should be ultimately cleaned up to
     // only listen from a front by bug 1465635.
     if (this.activeTab) {
@@ -695,22 +694,22 @@ Target.prototype = {
         }
       };
       this.client.addListener("tabDetached", this._onTabDetached);
 
       this._onSourceUpdated = (type, packet) => this.emit("source-updated", packet);
       this.client.addListener("newSource", this._onSourceUpdated);
       this.client.addListener("updatedSource", this._onSourceUpdated);
     }
-  },
+  }
 
   /**
    * Teardown listeners for remote debugging.
    */
-  _teardownRemoteListeners: function() {
+  _teardownRemoteListeners() {
     // Remove listeners set in _setupRemoteListeners
     this.client.removeListener("closed", this.destroy);
     if (this.activeTab) {
       this.activeTab.off("tabDetached", this.destroy);
       this.activeTab.off("newSource", this._onSourceUpdated);
       this.activeTab.off("updatedSource", this._onSourceUpdated);
     } else {
       this.client.removeListener("tabDetached", this._onTabDetached);
@@ -723,39 +722,39 @@ Target.prototype = {
       this.activeTab.off("tabNavigated", this._onTabNavigated);
       this.activeTab.off("frameUpdate", this._onFrameUpdate);
     }
 
     // Remove listeners set in attachConsole
     if (this.activeConsole && this._onInspectObject) {
       this.activeConsole.off("inspectObject", this._onInspectObject);
     }
-  },
+  }
 
   /**
    * Handle tabs events.
    */
-  handleEvent: function(event) {
+  handleEvent(event) {
     switch (event.type) {
       case "TabClose":
       case "unload":
         this.destroy();
         break;
       case "TabRemotenessChange":
         this.onRemotenessChange();
         break;
     }
-  },
+  }
 
   /**
    * Automatically respawn the toolbox when the tab changes between being
    * loaded within the parent process and loaded from a content process.
    * Process change can go in both ways.
    */
-  onRemotenessChange: function() {
+  onRemotenessChange() {
     // Responsive design do a crazy dance around tabs and triggers
     // remotenesschange events. But we should ignore them as at the end
     // the content doesn't change its remoteness.
     if (this._tab.isResponsiveDesignMode) {
       return;
     }
 
     // Save a reference to the tab as it will be nullified on destroy
@@ -766,22 +765,22 @@ Target.prototype = {
       }
       gDevTools.off("toolbox-destroyed", target);
 
       // Recreate a fresh target instance as the current one is now destroyed
       const newTarget = await TargetFactory.forTab(tab);
       gDevTools.showToolbox(newTarget);
     };
     gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
-  },
+  }
 
   /**
    * Target is not alive anymore.
    */
-  destroy: function() {
+  destroy() {
     // If several things call destroy then we give them all the same
     // destruction promise so we're sure to destroy only once
     if (this._destroyer) {
       return this._destroyer;
     }
 
     this._destroyer = (async () => {
       // Before taking any action, notify listeners that destruction is imminent.
@@ -813,64 +812,65 @@ Target.prototype = {
           console.warn(`Error while detaching target: ${e.message}`);
         }
       }
 
       this._cleanup();
     })();
 
     return this._destroyer;
-  },
+  }
 
   /**
    * Clean up references to what this target points to.
    */
-  _cleanup: function() {
+  _cleanup() {
     if (this._tab) {
       targets.delete(this._tab);
     } else {
       promiseTargets.delete(this.form);
     }
 
     this.activeTab = null;
     this.activeConsole = null;
     this._client = null;
     this._tab = null;
     this._attach = null;
     this._title = null;
     this._url = null;
-  },
+  }
 
-  toString: function() {
+  toString() {
     const id = this._tab ? this._tab : (this.form && this.form.actor);
     return `Target:${id}`;
-  },
+  }
 
   /**
    * Log an error of some kind to the tab's console.
    *
    * @param {String} text
    *                 The text to log.
    * @param {String} category
    *                 The category of the message.  @see nsIScriptError.
    */
-  logErrorInPage: function(text, category) {
+  logErrorInPage(text, category) {
     if (this.activeTab && this.activeTab.traits.logInPage) {
       const errorFlag = 0;
       this.activeTab.logInPage({ text, category, flags: errorFlag });
     }
-  },
+  }
 
   /**
    * Log a warning of some kind to the tab's console.
    *
    * @param {String} text
    *                 The text to log.
    * @param {String} category
    *                 The category of the message.  @see nsIScriptError.
    */
-  logWarningInPage: function(text, category) {
+  logWarningInPage(text, category) {
     if (this.activeTab && this.activeTab.traits.logInPage) {
       const warningFlag = 1;
       this.activeTab.logInPage({ text, category, flags: warningFlag });
     }
-  },
-};
+  }
+}
+exports.Target = Target;
--- a/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
+++ b/devtools/client/framework/test/browser_toolbox_options_disable_buttons.js
@@ -16,16 +16,17 @@ var doc = null, toolbox = null, panelWin
 
 function test() {
   addTab(TEST_URL).then(async (tab) => {
     const target = await TargetFactory.forTab(tab);
     gDevTools.showToolbox(target)
       .then(testSelectTool)
       .then(testToggleToolboxButtons)
       .then(testPrefsAreRespectedWhenReopeningToolbox)
+      .then(testButtonStateOnClick)
       .then(cleanup, errorHandler);
   });
 }
 
 async function testPrefsAreRespectedWhenReopeningToolbox() {
   const target = await TargetFactory.forTab(gBrowser.selectedTab);
 
   return new Promise(resolve => {
@@ -74,16 +75,36 @@ function testPreferenceAndUIStateIsConsi
     const check = checkNodes.filter(node => node.id === tool.id)[0];
     if (check) {
       is(check.checked, isVisible,
         "Checkbox should be selected based on current pref for " + tool.id);
     }
   }
 }
 
+async function testButtonStateOnClick() {
+  const toolboxButtons = ["#command-button-rulers", "#command-button-measure"];
+  for (const toolboxButton of toolboxButtons) {
+    const button = doc.querySelector(toolboxButton);
+    if (button) {
+      const isChecked = waitUntil(() => button.classList.contains("checked"));
+
+      button.click();
+      await isChecked;
+      ok(button.classList.contains("checked"),
+        `Button for ${toolboxButton} can be toggled on`);
+
+      const isUnchecked = waitUntil(() => !button.classList.contains("checked"));
+      button.click();
+      await isUnchecked;
+      ok(!button.classList.contains("checked"),
+        `Button for ${toolboxButton} can be toggled off`);
+    }
+  }
+}
 function testToggleToolboxButtons() {
   const checkNodes = [...panelWin.document.querySelectorAll(
     "#enabled-toolbox-buttons-box input[type=checkbox]")];
 
   const visibleToolbarButtons = toolbox.toolbarButtons.filter(tool => tool.isVisible);
 
   const toolbarButtonNodes = [...doc.querySelectorAll(".command-button")];
 
--- a/devtools/client/inspector/markup/test/browser.ini
+++ b/devtools/client/inspector/markup/test/browser.ini
@@ -171,17 +171,16 @@ skip-if = verify
 [browser_markup_node_not_displayed_02.js]
 [browser_markup_pagesize_01.js]
 [browser_markup_pagesize_02.js]
 [browser_markup_pseudo_on_reload.js]
 [browser_markup_remove_xul_attributes.js]
 [browser_markup_screenshot_node.js]
 [browser_markup_screenshot_node_iframe.js]
 [browser_markup_screenshot_node_shadowdom.js]
-skip-if = os == "win" && !debug # skip on Windows opt/pgo platforms Bug 1508435
 [browser_markup_search_01.js]
 [browser_markup_shadowdom.js]
 [browser_markup_shadowdom_clickreveal.js]
 [browser_markup_shadowdom_clickreveal_scroll.js]
 [browser_markup_shadowdom_copy_paths.js]
 subsuite = clipboard
 [browser_markup_shadowdom_delete.js]
 [browser_markup_shadowdom_dynamic.js]
--- a/devtools/client/inspector/markup/test/helper_screenshot_node.js
+++ b/devtools/client/inspector/markup/test/helper_screenshot_node.js
@@ -61,16 +61,23 @@ async function takeNodeScreenshot(inspec
   info("Create an image using the downloaded fileas source");
   const image = new Image();
   image.src = OS.Path.toFileURI(filePath);
   await once(image, "load");
 
   info("Remove the downloaded screenshot file");
   await OS.File.remove(filePath);
 
+  // See intermittent Bug 1508435. Even after removing the file, tests still manage to
+  // reuse files from the previous test if they have the same name. Since our file name
+  // is based on a timestamp that has "second" precision, wait for one second to make sure
+  // screenshots will have different names.
+  info("Wait for one second to make sure future screenshots will use a different name");
+  await new Promise(r => setTimeout(r, 1000));
+
   return image;
 }
 /* exported takeNodeScreenshot */
 
 /**
  * Check that the provided image has the expected width, height, and color.
  * NOTE: This test assumes that the image is only made of a single color and will only
  * check one pixel.
--- a/dom/animation/test/chrome/test_animation_properties.html
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -560,17 +560,17 @@ var gTests = [
     frames:   { left: ['10em', '20em'] },
     expected: [ { property: 'left',
                   values: [ valueFormat(0, '100px', 'replace', 'linear'),
                             valueFormat(1, '200px', 'replace') ] } ]
   },
   { desc:     'calc() expressions are resolved to the equivalent units',
     frames:   { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
     expected: [ { property: 'left',
-                  values: [ valueFormat(0, 'calc(110px)', 'replace', 'linear'),
+                  values: [ valueFormat(0, '110px', 'replace', 'linear'),
                             valueFormat(1, 'calc(10% + 100px)', 'replace') ] } ]
   },
 
   // ---------------------------------------------------------------------
   //
   // Tests for CSS variable handling conversion
   //
   // ---------------------------------------------------------------------
new file mode 100644
--- /dev/null
+++ b/dom/base/ScriptableContentIterator.cpp
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScriptableContentIterator.h"
+#include "nsINode.h"
+#include "nsRange.h"
+
+namespace mozilla {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ScriptableContentIterator)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ScriptableContentIterator)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScriptableContentIterator)
+  NS_INTERFACE_MAP_ENTRY(nsIScriptableContentIterator)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(ScriptableContentIterator, mContentIterator)
+
+ScriptableContentIterator::ScriptableContentIterator()
+    : mIteratorType(NOT_INITIALIZED) {}
+
+void ScriptableContentIterator::EnsureContentIterator() {
+  if (mContentIterator) {
+    return;
+  }
+  switch (mIteratorType) {
+    case POST_ORDER_ITERATOR:
+    default:
+      mContentIterator = NS_NewContentIterator();
+      break;
+    case PRE_ORDER_ITERATOR:
+      mContentIterator = NS_NewPreContentIterator();
+      break;
+    case SUBTREE_ITERATOR:
+      mContentIterator = NS_NewContentSubtreeIterator();
+      break;
+  }
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::InitWithRootNode(IteratorType aType,
+                                            nsINode* aRoot) {
+  if (aType == NOT_INITIALIZED ||
+      (mIteratorType != NOT_INITIALIZED && aType != mIteratorType)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  mIteratorType = aType;
+  EnsureContentIterator();
+  return mContentIterator->Init(aRoot);
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::InitWithRange(IteratorType aType, nsRange* aRange) {
+  if (aType == NOT_INITIALIZED ||
+      (mIteratorType != NOT_INITIALIZED && aType != mIteratorType)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  mIteratorType = aType;
+  EnsureContentIterator();
+  return mContentIterator->Init(aRange);
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::InitWithPositions(IteratorType aType,
+                                             nsINode* aStartContainer,
+                                             uint32_t aStartOffset,
+                                             nsINode* aEndContainer,
+                                             uint32_t aEndOffset) {
+  if (aType == NOT_INITIALIZED ||
+      (mIteratorType != NOT_INITIALIZED && aType != mIteratorType)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  mIteratorType = aType;
+  EnsureContentIterator();
+  return mContentIterator->Init(aStartContainer, aStartOffset, aEndContainer,
+                                aEndOffset);
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::First() {
+  if (!mContentIterator) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  mContentIterator->First();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::Last() {
+  if (!mContentIterator) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  mContentIterator->Last();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::Next() {
+  if (!mContentIterator) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  mContentIterator->Next();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::Prev() {
+  if (!mContentIterator) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  mContentIterator->Prev();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::GetCurrentNode(nsINode** aNode) {
+  if (!mContentIterator) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  NS_IF_ADDREF(*aNode = mContentIterator->GetCurrentNode());
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::GetIsDone(bool* aIsDone) {
+  if (!mContentIterator) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  *aIsDone = mContentIterator->IsDone();
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+ScriptableContentIterator::PositionAt(nsINode* aNode) {
+  if (!mContentIterator) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  return mContentIterator->PositionAt(aNode);
+}
+
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/ScriptableContentIterator.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_scriptablecontentiterator_h
+#define mozilla_scriptablecontentiterator_h
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsIContentIterator.h"
+#include "nsIScriptableContentIterator.h"
+
+namespace mozilla {
+
+class ScriptableContentIterator final : public nsIScriptableContentIterator {
+ public:
+  ScriptableContentIterator();
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS(ScriptableContentIterator)
+  NS_DECL_NSISCRIPTABLECONTENTITERATOR
+
+ protected:
+  virtual ~ScriptableContentIterator() = default;
+  void EnsureContentIterator();
+
+  IteratorType mIteratorType;
+  nsCOMPtr<nsIContentIterator> mContentIterator;
+};
+
+}  // namespace mozilla
+
+#endif  // #ifndef mozilla_scriptablecontentiterator_h
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -18,16 +18,17 @@ XPIDL_SOURCES += [
     'nsIDocumentEncoder.idl',
     'nsIDOMRequestService.idl',
     'nsIDroppedLinkHandler.idl',
     'nsIFrameLoaderOwner.idl',
     'nsIImageLoadingContent.idl',
     'nsIMessageManager.idl',
     'nsIObjectLoadingContent.idl',
     'nsIRemoteWindowContext.idl',
+    'nsIScriptableContentIterator.idl',
     'nsIScriptChannel.idl',
     'nsISelectionController.idl',
     'nsISelectionDisplay.idl',
     'nsISelectionListener.idl',
     'nsISlowScriptDebug.idl',
 ]
 
 XPIDL_MODULE = 'dom'
@@ -124,16 +125,17 @@ if CONFIG['MOZ_WEBRTC']:
         'nsDOMDataChannelDeclarations.h',
     ]
 
 EXPORTS.mozilla += [
     'CORSMode.h',
     'FlushType.h',
     'FullscreenChange.h',
     'RangeBoundary.h',
+    'ScriptableContentIterator.h',
     'SelectionChangeEventDispatcher.h',
     'TextInputProcessor.h',
     'UseCounter.h',
 ]
 
 EXPORTS.mozilla.dom += [
     '!UseCounterList.h',
     'AnonymousContent.h',
@@ -370,16 +372,17 @@ UNIFIED_SOURCES += [
     'Pose.cpp',
     'PostMessageEvent.cpp',
     'ProcessMessageManager.cpp',
     'RemoteOuterWindowProxy.cpp',
     'ResponsiveImageSelector.cpp',
     'SameProcessMessageQueue.cpp',
     'ScreenLuminance.cpp',
     'ScreenOrientation.cpp',
+    'ScriptableContentIterator.cpp',
     'Selection.cpp',
     'SelectionChangeEventDispatcher.cpp',
     'ShadowRoot.cpp',
     'StorageAccessPermissionRequest.cpp',
     'StructuredCloneBlob.cpp',
     'StructuredCloneHolder.cpp',
     'StructuredCloneTester.cpp',
     'StyleSheetList.cpp',
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -4099,21 +4099,23 @@ nsresult nsContentUtils::DispatchEvent(D
     *aDefaultAction = (status != nsEventStatus_eConsumeNoDefault);
   }
   return rv;
 }
 
 // static
 nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement) {
   RefPtr<TextEditor> textEditor;  // See bug 1506439
-  return DispatchInputEvent(aEventTargetElement, textEditor);
+  return DispatchInputEvent(aEventTargetElement, EditorInputType::eUnknown,
+                            textEditor);
 }
 
 // static
 nsresult nsContentUtils::DispatchInputEvent(Element* aEventTargetElement,
+                                            EditorInputType aEditorInputType,
                                             TextEditor* aTextEditor) {
   if (NS_WARN_IF(!aEventTargetElement)) {
     return NS_ERROR_INVALID_ARG;
   }
 
   // If this is called from editor, the instance should be set to aTextEditor.
   // Otherwise, we need to look for an editor for aEventTargetElement.
   // However, we don't need to do it for HTMLEditor since nobody shouldn't
@@ -4137,16 +4139,17 @@ nsresult nsContentUtils::DispatchInputEv
     nsCOMPtr<nsITextControlElement> textControlElement =
         do_QueryInterface(aEventTargetElement);
     MOZ_ASSERT(!textControlElement,
                "The event target may have editor, but we've not known it yet.");
   }
 #endif  // #ifdef DEBUG
 
   if (!useInputEvent) {
+    MOZ_ASSERT(aEditorInputType == EditorInputType::eUnknown);
     // Dispatch "input" event with Event instance.
     WidgetEvent widgetEvent(true, eUnidentifiedEvent);
     widgetEvent.mSpecifiedEventType = nsGkAtoms::oninput;
     widgetEvent.mFlags.mCancelable = false;
     // Using same time as nsContentUtils::DispatchEvent() for backward
     // compatibility.
     widgetEvent.mTime = PR_Now();
     (new AsyncEventDispatcher(aEventTargetElement, widgetEvent))
@@ -4191,16 +4194,18 @@ nsresult nsContentUtils::DispatchInputEv
   // Note that EditorBase::IsIMEComposing() may return false even when we
   // need to set it to true.
   // Otherwise, i.e., editor hasn't been created for the element yet,
   // we should set isComposing to false since the element can never has
   // composition without editor.
   inputEvent.mIsComposing =
       aTextEditor ? !!aTextEditor->GetComposition() : false;
 
+  inputEvent.mInputType = aEditorInputType;
+
   (new AsyncEventDispatcher(aEventTargetElement, inputEvent))
       ->RunDOMEventWhenSafe();
   return NS_OK;
 }
 
 nsresult nsContentUtils::DispatchChromeEvent(
     Document* aDoc, nsISupports* aTarget, const nsAString& aEventName,
     CanBubble aCanBubble, Cancelable aCancelable, bool* aDefaultAction) {
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1392,24 +1392,28 @@ class nsContentUtils {
    * unsafe to dispatch, this put the event into the script runner queue.
    * Input Events spec defines as:
    *   Input events are dispatched on elements that act as editing hosts,
    *   including elements with the contenteditable attribute set, textarea
    *   elements, and input elements that permit text input.
    *
    * @param aEventTarget        The event target element of the "input" event.
    *                            Must not be nullptr.
+   * @param aEditorInputType    The inputType value of InputEvent.
+   *                            If aEventTarget won't dispatch "input" event
+   *                            with InputEvent, set EditorInputType::eUnknown.
    * @param aTextEditor         Optional.  If this is called by editor,
    *                            editor should set this.  Otherwise, leave
    *                            nullptr.
    */
   MOZ_CAN_RUN_SCRIPT
   static nsresult DispatchInputEvent(Element* aEventTarget);
   MOZ_CAN_RUN_SCRIPT
   static nsresult DispatchInputEvent(Element* aEventTarget,
+                                     mozilla::EditorInputType aEditorInputType,
                                      mozilla::TextEditor* aTextEditor);
 
   /**
    * This method creates and dispatches a untrusted event.
    * Works only with events which can be created by calling
    * Document::CreateEvent() with parameter "Events".
    * @param aDoc           The document which will be used to create the event.
    * @param aTarget        The target of the event, should be QIable to
new file mode 100644
--- /dev/null
+++ b/dom/base/nsIScriptableContentIterator.idl
@@ -0,0 +1,74 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+webidl Node;
+webidl Range;
+
+/**
+ * nsIScriptableContentIterator is designed to testing concrete classes of
+ * nsIContentIterator.
+ */
+[scriptable, builtinclass, uuid(9f25fb2a-265f-44f9-a122-62bbf443239e)]
+interface nsIScriptableContentIterator : nsISupports
+{
+  cenum IteratorType : 8 {
+    NOT_INITIALIZED,
+    POST_ORDER_ITERATOR,
+    PRE_ORDER_ITERATOR,
+    SUBTREE_ITERATOR
+  };
+
+  /**
+   * You need to call initWith*() first.  Then, the instance of this interface
+   * decides the type of iterator with its aType argument.  You can call
+   * initWith*() multiple times, but you need to keep setting same type as
+   * previous call.  If you set different type, these method with throw an
+   * exception.
+   */
+
+  // See nsIContentIterator::Init(nsINode*)
+  void initWithRootNode(in nsIScriptableContentIterator_IteratorType aType,
+                        in Node aRoot);
+
+  // See nsIContentIterator::Init(nsRange*)
+  void initWithRange(in nsIScriptableContentIterator_IteratorType aType,
+                     in Range aRange);
+
+  // See nsIContentIterator::Init(nsINode*, uint32_t, nsINode*, uint32_t)
+  void initWithPositions(in nsIScriptableContentIterator_IteratorType aType,
+                         in Node aStartContainer, in unsigned long aStartOffset,
+                         in Node aEndContainer, in unsigned long aEndOffset);
+
+  // See nsIContentIterator::First()
+  void first();
+
+  // See nsIContentIterator::Last()
+  void last();
+
+  // See nsIContentIterator::Next()
+  void next();
+
+  // See nsIContentIterator::Prev()
+  void prev();
+
+  // See nsIContentIterator::GetCurrentNode()
+  readonly attribute Node currentNode;
+
+  // See nsIContentIterator::IsDone()
+  readonly attribute bool isDone;
+
+  // See nsIContentIterator::PositionAt(nsINode*)
+  void positionAt(in Node aNode);
+};
+
+%{C++
+#define SCRIPTABLE_CONTENT_ITERATOR_CID \
+  { 0xf68037ec, 0x2790, 0x44c5, \
+    { 0x8e, 0x5f, 0xdf, 0x5d, 0xa5, 0x8b, 0x93, 0xa7 } }
+#define SCRIPTABLE_CONTENT_ITERATOR_CONTRACTID \
+  "@mozilla.org/scriptable-content-iterator;1"
+%}
--- a/dom/base/test/chrome/window_nsITextInputProcessor.xul
+++ b/dom/base/test/chrome/window_nsITextInputProcessor.xul
@@ -54,24 +54,25 @@ function finish()
   window.close();
 }
 
 function onunload()
 {
   SimpleTest.finish();
 }
 
-function checkInputEvent(aEvent, aIsComposing, aDescription) {
+function checkInputEvent(aEvent, aIsComposing, aInputType, aDescription) {
   if (aEvent.type != "input") {
     return;
   }
   ok(aEvent instanceof InputEvent, `${aDescription}"input" event should be dispatched with InputEvent interface`);
   is(aEvent.cancelable, false, `${aDescription}"input" event should be never cancelable`);
   is(aEvent.bubbles, true, `${aDescription}"input" event should always bubble`);
   is(aEvent.isComposing, aIsComposing, `${aDescription}isComposing should be ${aIsComposing}`);
+  is(aEvent.inputType, aInputType, `${aDescription}inputType should be "${aInputType}"`);
 }
 
 const kIsMac = (navigator.platform.indexOf("Mac") == 0);
 
 var iframe = document.getElementById("iframe");
 var childWindow = iframe.contentWindow;
 var textareaInFrame;
 var input = document.getElementById("input");
@@ -227,17 +228,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], true, description);
+  checkInputEvent(events[3], true, "insertCompositionText", description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -263,17 +264,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransaction(window, simpleCallback),
        description + "TIP2 shouldn't be able to begin input transaction from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");");
@@ -311,17 +312,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
-  checkInputEvent(events[4], false, description);
+  checkInputEvent(events[4], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during cancelComposition().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -354,17 +355,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], false, description);
+  checkInputEvent(events[3], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransaction() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransaction(window, simpleCallback),
@@ -394,17 +395,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertText", description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during startComposition().
   var events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
@@ -452,17 +453,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], true, description);
+  checkInputEvent(events[3], true, "insertCompositionText", description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -488,17 +489,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during commitCompositionWith("bar").
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransactionForTests(window),
        description + "TIP2 shouldn't be able to begin input transaction for tests from compositionstart event handler during TIP1.commitCompositionWith(\"bar\");");
@@ -536,17 +537,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
-  checkInputEvent(events[4], false, description);
+  checkInputEvent(events[4], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during cancelComposition().
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -579,17 +580,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], false, description);
+  checkInputEvent(events[3], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransactionForTests() fails to steal the rights of TextEventDispatcher during keydown() and keyup().
   events = [];
   TIP1.beginInputTransactionForTests(window);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     ok(!TIP2.beginInputTransactionForTests(window),
@@ -619,17 +620,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertText", description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition().
   var events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
@@ -707,17 +708,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], true, description);
+  checkInputEvent(events[3], true, "insertCompositionText", description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -761,17 +762,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransaction(otherWindow, simpleCallback);
@@ -839,17 +840,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
-  checkInputEvent(events[4], false, description);
+  checkInputEvent(events[4], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -906,17 +907,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], false, description);
+  checkInputEvent(events[3], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransaction() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
   events = [];
   TIP1.beginInputTransaction(window, simpleCallback);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
@@ -970,17 +971,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertText", description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during startComposition().
   var events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
@@ -1058,17 +1059,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionstart",
      description + "events[0] should be compositionstart");
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], true, description);
+  checkInputEvent(events[3], true, "insertCompositionText", description);
   TIP1.cancelComposition();
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitComposition().
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
@@ -1112,17 +1113,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 3,
      description + "text, compositionend and input events should be fired by TIP1.commitComposition()");
   is(events[0].type, "text",
      description + "events[0] should be text");
   is(events[1].type, "compositionend",
      description + "events[1] should be compositionend");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during commitCompositionWith("bar");.
   events = [];
   input.addEventListener("compositionstart", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
       TIP1.beginInputTransactionForTests(otherWindow, simpleCallback);
@@ -1190,17 +1191,17 @@ function runBeginInputTransactionMethodT
   is(events[1].type, "compositionupdate",
      description + "events[1] should be compositionupdate");
   is(events[2].type, "text",
      description + "events[2] should be text");
   is(events[3].type, "compositionend",
      description + "events[3] should be compositionend");
   is(events[4].type, "input",
      description + "events[4] should be input");
-  checkInputEvent(events[4], false, description);
+  checkInputEvent(events[4], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during cancelComposition();.
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   TIP1.setPendingCompositionString(composingStr);
   TIP1.appendClauseToPendingComposition(composingStr.length, TIP1.ATTR_RAW_CLAUSE);
   TIP1.flushPendingComposition();
   input.addEventListener("compositionupdate", function (aEvent) {
@@ -1257,17 +1258,17 @@ function runBeginInputTransactionMethodT
   is(events[0].type, "compositionupdate",
      description + "events[0] should be compositionupdate");
   is(events[1].type, "text",
      description + "events[1] should be text");
   is(events[2].type, "compositionend",
      description + "events[2] should be compositionend");
   is(events[3].type, "input",
      description + "events[3] should be input");
-  checkInputEvent(events[3], false, description);
+  checkInputEvent(events[3], false, "insertCompositionText", description);
 
   // Let's check if beginInputTransactionForTests() with another window fails to begin new input transaction with different TextEventDispatcher during keydown() and keyup();.
   events = [];
   TIP1.beginInputTransactionForTests(window, simpleCallback);
   input.addEventListener("keydown", function (aEvent) {
     events.push(aEvent);
     input.removeEventListener(aEvent.type, arguments.callee, false);
     try {
@@ -1321,17 +1322,17 @@ function runBeginInputTransactionMethodT
   is(events.length, 4,
      description + "keydown, keypress, input, keyup events should be fired by TIP1.keydown() and TIP1.keyup()");
   is(events[0].type, "keydown",
      description + "events[0] should be keydown");
   is(events[1].type, "keypress",
      description + "events[1] should be keypress");
   is(events[2].type, "input",
      description + "events[2] should be input");
-  checkInputEvent(events[2], false, description);
+  checkInputEvent(events[2], false, "insertText", description);
   is(events[3].type, "keyup",
      description + "events[3] should be keyup");
 
   // Let's check if startComposition() throws an exception after ownership is stolen.
   input.value = "";
   ok(TIP1.beginInputTransactionForTests(window),
      description + "TIP1.beginInputTransactionForTests() should succeed because there is no composition");
   ok(TIP2.beginInputTransactionForTests(window),
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -617,16 +617,19 @@ skip-if = os == "mac"
 [test_bug1472427.html]
 [test_bug1499169.html]
 skip-if = toolkit == 'android' # Timeouts on android due to page closing issues with embedded pdf
 [test_caretPositionFromPoint.html]
 [test_change_policy.html]
 [test_clearTimeoutIntervalNoArg.html]
 [test_constructor-assignment.html]
 [test_constructor.html]
+[test_content_iterator_post_order.html]
+[test_content_iterator_pre_order.html]
+[test_content_iterator_subtree.html]
 [test_copyimage.html]
 subsuite = clipboard
 skip-if = toolkit == 'android' #bug 904183
 [test_copypaste.html]
 subsuite = clipboard
 skip-if = toolkit == 'android' #bug 904183
 [test_copypaste.xhtml]
 subsuite = clipboard
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_content_iterator_post_order.html
@@ -0,0 +1,875 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for post-order content iterator</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+function finish() {
+  // The SimpleTest may require usual elements in the template, but they shouldn't be during test.
+  // So, let's create them at end of the test.
+  document.body.innerHTML = '<div id="display"></div><div id="content"></div><pre id="test"></pre>';
+  SimpleTest.finish();
+}
+
+function createContentIterator() {
+  return Cc["@mozilla.org/scriptable-content-iterator;1"]
+      .createInstance(Ci.nsIScriptableContentIterator);
+}
+
+function getNodeDescription(aNode) {
+  if (aNode === undefined) {
+    return "undefine";
+  }
+  if (aNode === null) {
+    return "null";
+  }
+  function getElementDescription(aElement) {
+    if (aElement.tagName === "BR") {
+      if (aElement.previousSibling) {
+        return `<br> element after ${getNodeDescription(aElement.previousSibling)}`;
+      }
+      return `<br> element in ${getElementDescription(aElement.parentElement)}`;
+    }
+    let hasHint = aElement == document.body;
+    let tag = `<${aElement.tagName.toLowerCase()}`;
+    if (aElement.getAttribute("id")) {
+      tag += ` id="${aElement.getAttribute("id")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("class")) {
+      tag += ` class="${aElement.getAttribute("class")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("type")) {
+      tag += ` type="${aElement.getAttribute("type")}"`;
+    }
+    if (aElement.getAttribute("name")) {
+      tag += ` name="${aElement.getAttribute("name")}"`;
+    }
+    if (aElement.getAttribute("value")) {
+      tag += ` value="${aElement.getAttribute("value")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("style")) {
+      tag += ` style="${aElement.getAttribute("style")}"`;
+      hasHint = true;
+    }
+    if (hasHint) {
+      return tag + ">";
+    }
+    return `${tag}> in ${getElementDescription(aElement.parentElement)}`;
+  }
+  switch (aNode.nodeType) {
+    case aNode.TEXT_NODE:
+      return `text node, "${aNode.wholeText.replace(/\n/g, '\\n')}"`;
+    case aNode.COMMENT_NODE:
+      return `comment node, "${aNode.data.replace(/\n/g, '\\n')}"`;
+    case aNode.ELEMENT_NODE:
+      return getElementDescription(SpecialPowers.unwrap(aNode));
+    default:
+      return "unknown node";
+  }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function () {
+  let iter = createContentIterator();
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with an empty element.
+   */
+  document.body.innerHTML = "<div></div>";
+  let description = "Initialized with empty <div> as root node:";
+  iter.initWithRootNode(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, document.body.firstChild);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with a range which selects empty element.
+   */
+  let range = document.createRange();
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with range including only empty <div>:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with positions which select empty element.
+   */
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with positions including only empty <div>:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with collapsed range in an empty element.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild, 0);
+  description = "Initialized with range collapsed in empty <div>:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range in an empty element.
+   */
+  description = "Initialized with a position in empty <div>:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         document.body.firstChild, 0, document.body.firstChild, 0);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with the text element.
+   */
+  document.body.innerHTML = "<div>some text.</div>";
+  description = "Initialized with a text node as root node:";
+  iter.initWithRootNode(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, document.body.firstChild.firstChild);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be the text node after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with a range which selects the text node.
+   */
+  range = document.createRange();
+  range.selectNode(document.body.firstChild.firstChild);
+  description = "Initialized with range including only text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first() and next() after initialized with positions which select the text node.
+   * XXX In this case, content iterator lists up the parent <div> element.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with positions including only text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> element after calling next() from first position (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling next() from first position`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() from second position (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next() from second position`);
+
+  /**
+   * Tests to initializing with collapsed range at start of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, 0);
+  description = "Initialized with range collapsed at start of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at start of a text node.
+   * XXX In this case, content iterator lists up the text node.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  description = "Initialized with a position at start of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, 0, document.body.firstChild.firstChild, 0);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with collapsed range at end of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  description = "Initialized with range collapsed at end of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at end of a text node.
+   * XXX In this case, content iterator lists up the text node.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  description = "Initialized with a position at end of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with collapsed range at middle of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2);
+  description = "Initialized with range collapsed at end of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at middle of a text node.
+   * XXX In this case, content iterator lists up the text node.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  description = "Initialized with a position at end of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with a range selecting all text in a text node.
+   */
+  range = document.createRange();
+  range.setStart(document.body.firstChild.firstChild, 0);
+  range.setEnd(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  description = "Initialized with range selecting all text in text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with positions selecting all text in a text node.
+   */
+  description = "Initialized with positions selecting all text in text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, 0,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic tests with complicated tree.
+   */
+  function check(aIter, aExpectedResult, aDescription) {
+    if (aExpectedResult.length > 0) {
+      is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+        `${aDescription}: currentNode should be the text node immediately after initialization (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+      ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true immediately after initialization`);
+
+      aIter.first();
+      is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+        `${aDescription}: currentNode should be the text node after calling first() (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+      ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true after calling first()`);
+
+      for (let expected of aExpectedResult) {
+        is(SpecialPowers.unwrap(aIter.currentNode), expected,
+          `${aDescription}: currentNode should be the node (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(expected)})`);
+        ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true when ${getNodeDescription(expected)} is expected`);
+        aIter.next();
+      }
+
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null after calling next() finally (got: ${getNodeDescription(aIter.currentNode)}`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true after calling next() finally`);
+    } else {
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null immediately after initialization (got: ${getNodeDescription(aIter.currentNode)})`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true immediately after initialization`);
+
+      aIter.first();
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null after calling first() (got: ${getNodeDescription(aIter.currentNode)})`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true after calling first()`);
+    }
+  }
+
+  document.body.innerHTML = "<p>" +
+                              "Here is <b>bold</b> and <i><u>underlined and </u>italic </i><span>or no style text.</span><br>" +
+                            "</p>" +
+                            "<p>" +
+                              "Here is an &lt;input&gt; element: <input type=\"text\" value=\"default value\"><br>\n" +
+                              "and a &lt;textarea&gt; element: <textarea>text area's text node</textarea><br><br>\n" +
+                              "<!-- and here is comment node -->" +
+                            "</p>";
+
+  let expectedResult =
+    [document.body.firstChild.firstChild, // the first text node
+     document.body.firstChild.firstChild.nextSibling.firstChild, // text in <b>
+     document.body.firstChild.firstChild.nextSibling, // <b>
+     document.body.firstChild.firstChild.nextSibling.nextSibling, // text next to <b>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild.firstChild, // text in <u>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, // <u>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild.nextSibling, // text next to <u>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling, // <i>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.firstChild, // text in <span>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.nextSibling, // <span>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // <br> next to <span>
+     document.body.firstChild, // first <p>
+     document.body.firstChild.nextSibling.firstChild, // the first text node in second <p>
+     document.body.firstChild.nextSibling.firstChild.nextSibling, // <input>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling, // <br> next to <input>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling, // text next to <input>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.firstChild, // text in <textarea>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling, // <textarea>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // <br> next to <textarea>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // <br> next to <br>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // text next to <br>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // comment
+     document.body.firstChild.nextSibling, // second <p>
+     document.body]; // <body>
+
+  iter.initWithRootNode(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, document.body);
+  check(iter, expectedResult, "Initialized with the <body> as root element:");
+
+  /**
+   * Selects the <body> with a range.
+   */
+  range = document.createRange();
+  range.selectNode(document.body);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter, expectedResult, "Initialized with range selecting the <body>");
+
+  /**
+   * Selects the <body> with positions.
+   */
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset, range.endContainer, range.endOffset);
+  check(iter, expectedResult, "Initialized with positions selecting the <body>");
+
+  /**
+   * Selects all children in the <body> with a range.
+   */
+  expectedResult.pop(); // <body> shouldn't be listed up.
+  range = document.createRange();
+  range.selectNodeContents(document.body);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter, expectedResult, "Initialized with range selecting all children in the <body>");
+
+  /**
+   * Selects all children in the <body> with positions.
+   */
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset, range.endContainer, range.endOffset);
+  check(iter, expectedResult, "Initialized with positions selecting all children in the <body>");
+
+  /**
+   * range/positions around elements.
+   */
+  document.body.innerHTML = "abc<b>def</b><i>ghi</i>jkl";
+  range = document.createRange();
+
+  range.setStart(document.body.firstChild, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '[abc<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '[abc<b>de]f'");
+
+  range.setStart(document.body.firstChild, 2);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting 'ab[c<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting 'ab[c<b>de]f'");
+
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting 'abc[<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting 'abc[<b>de]f'");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting 'abc{<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting 'abc{<b>de]f'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{de]f'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{def]</b>'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{def]</b>'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling, 1);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{def}</b>'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{def}</b>'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild, // text in <b>
+         document.body.firstChild.nextSibling], // <b>
+        "Initialized with range selecting '<b>{def</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild, // text in <b>
+         document.body.firstChild.nextSibling], // <b>
+        "Initialized with positions selecting '<b>{def</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling.firstChild, 3);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild, // text in <b>
+         document.body.firstChild.nextSibling], // <b>
+        "Initialized with range selecting '<b>def[</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild, // text in <b>
+         document.body.firstChild.nextSibling], // <b>
+        "Initialized with positions selecting '<b>def[</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <b>
+        "Initialized with range selecting '<b>def{</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <b>
+        "Initialized with positions selecting '<b>def{</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <b>
+        "Initialized with range selecting '<b>def{</b><i>}ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <b>
+        "Initialized with positions selecting '<b>def{</b><i>}ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling.firstChild, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with range selecting '<b>def{</b><i>]ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with positions selecting '<b>def{</b><i>]ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 0);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild, // text in <i>
+         document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with range selecting '<i>{ghi</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild, // text in <i>
+         document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with positions selecting '<i>{ghi</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling.firstChild, 3);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild, // text in <i>
+         document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with range selecting '<i>ghi[</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild, // text in <i>
+         document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with positions selecting '<i>ghi[</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 1);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with range selecting '<i>ghi{</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with positions selecting '<i>ghi{</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling, // <i>
+         document.body.firstChild.nextSibling.nextSibling.nextSibling], // text after <i>
+        "Initialized with range selecting '<i>ghi{</i>]jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling, // <i>
+         document.body.firstChild.nextSibling.nextSibling.nextSibling], // text after <i>
+        "Initialized with positions selecting '<i>ghi{</i>]jkl'");
+
+  /**
+   * range/positions around <br> elements.
+   */
+  document.body.innerHTML = "abc<br>def";
+  range = document.createRange();
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <br>
+         document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with range selecting 'abc[<br>]def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <br>
+         document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with positions selecting 'abc[<br>]def'");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with range selecting 'abc{<br>]def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with positions selecting 'abc{<br>]def'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with range selecting 'abc{<br>]def' (starting in <br>)");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with positions selecting 'abc{<br>]def' (starting in <br>)");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with range selecting 'abc{<br>}def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with positions selecting 'abc{<br>}def'");
+
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild], // text before <br>
+        "Initialized with range selecting 'abc[}<br>def' (ending in <br>)");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.POST_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild], // text before <br>
+        "Initialized with positions selecting 'abc[}<br>def' (ending in <br>)");
+
+  finish();
+});
+</script>
+</head>
+<body></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_content_iterator_pre_order.html
@@ -0,0 +1,869 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for pre-order content iterator</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+function finish() {
+  // The SimpleTest may require usual elements in the template, but they shouldn't be during test.
+  // So, let's create them at end of the test.
+  document.body.innerHTML = '<div id="display"></div><div id="content"></div><pre id="test"></pre>';
+  SimpleTest.finish();
+}
+
+function createContentIterator() {
+  return Cc["@mozilla.org/scriptable-content-iterator;1"]
+      .createInstance(Ci.nsIScriptableContentIterator);
+}
+
+function getNodeDescription(aNode) {
+  if (aNode === undefined) {
+    return "undefine";
+  }
+  if (aNode === null) {
+    return "null";
+  }
+  function getElementDescription(aElement) {
+    if (aElement.tagName === "BR") {
+      if (aElement.previousSibling) {
+        return `<br> element after ${getNodeDescription(aElement.previousSibling)}`;
+      }
+      return `<br> element in ${getElementDescription(aElement.parentElement)}`;
+    }
+    let hasHint = aElement == document.body;
+    let tag = `<${aElement.tagName.toLowerCase()}`;
+    if (aElement.getAttribute("id")) {
+      tag += ` id="${aElement.getAttribute("id")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("class")) {
+      tag += ` class="${aElement.getAttribute("class")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("type")) {
+      tag += ` type="${aElement.getAttribute("type")}"`;
+    }
+    if (aElement.getAttribute("name")) {
+      tag += ` name="${aElement.getAttribute("name")}"`;
+    }
+    if (aElement.getAttribute("value")) {
+      tag += ` value="${aElement.getAttribute("value")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("style")) {
+      tag += ` style="${aElement.getAttribute("style")}"`;
+      hasHint = true;
+    }
+    if (hasHint) {
+      return tag + ">";
+    }
+    return `${tag}> in ${getElementDescription(aElement.parentElement)}`;
+  }
+  switch (aNode.nodeType) {
+    case aNode.TEXT_NODE:
+      return `text node, "${aNode.wholeText.replace(/\n/g, '\\n')}"`;
+    case aNode.COMMENT_NODE:
+      return `comment node, "${aNode.data.replace(/\n/g, '\\n')}"`;
+    case aNode.ELEMENT_NODE:
+      return getElementDescription(SpecialPowers.unwrap(aNode));
+    default:
+      return "unknown node";
+  }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function () {
+  let iter = createContentIterator();
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with an empty element.
+   */
+  document.body.innerHTML = "<div></div>";
+  let description = "Initialized with empty <div> as root node:";
+  iter.initWithRootNode(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, document.body.firstChild);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with a range which selects empty element.
+   */
+  let range = document.createRange();
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with range including only empty <div>:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with positions which select empty element.
+   */
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with positions including only empty <div>:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with collapsed range in an empty element.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild, 0);
+  description = "Initialized with range collapsed in empty <div>:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range in an empty element.
+   */
+  description = "Initialized with a position in empty <div>:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         document.body.firstChild, 0, document.body.firstChild, 0);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with the text element.
+   */
+  document.body.innerHTML = "<div>some text.</div>";
+  description = "Initialized with a text node as root node:";
+  iter.initWithRootNode(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, document.body.firstChild.firstChild);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be the text node after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with a range which selects the text node.
+   */
+  range = document.createRange();
+  range.selectNode(document.body.firstChild.firstChild);
+  description = "Initialized with range including only text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first() and next() after initialized with positions which select the text node.
+   * XXX In this case, content iterator lists up the parent <div> element.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with positions including only text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> element immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> element after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling next() from first position (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling next() from first position`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() from second position (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next() from second position`);
+
+  /**
+   * Tests to initializing with collapsed range at start of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, 0);
+  description = "Initialized with range collapsed at start of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at start of a text node.
+   * XXX In this case, content iterator lists up the text node.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  description = "Initialized with a position at start of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, 0, document.body.firstChild.firstChild, 0);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with collapsed range at end of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  description = "Initialized with range collapsed at end of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at end of a text node.
+   * XXX In this case, content iterator lists up the text node.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  description = "Initialized with a position at end of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with collapsed range at middle of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2);
+  description = "Initialized with range collapsed at end of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at middle of a text node.
+   * XXX In this case, content iterator lists up the text node.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  description = "Initialized with a position at end of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with a range selecting all text in a text node.
+   */
+  range = document.createRange();
+  range.setStart(document.body.firstChild.firstChild, 0);
+  range.setEnd(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  description = "Initialized with range selecting all text in text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with positions selecting all text in a text node.
+   */
+  description = "Initialized with positions selecting all text in text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         document.body.firstChild.firstChild, 0,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic tests with complicated tree.
+   */
+  function check(aIter, aExpectedResult, aDescription) {
+    if (aExpectedResult.length > 0) {
+      is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+        `${aDescription}: currentNode should be the text node immediately after initialization (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+      ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true immediately after initialization`);
+
+      aIter.first();
+      is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+        `${aDescription}: currentNode should be the text node after calling first() (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+      ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true after calling first()`);
+
+      for (let expected of aExpectedResult) {
+        is(SpecialPowers.unwrap(aIter.currentNode), expected,
+          `${aDescription}: currentNode should be the node (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(expected)})`);
+        ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true when ${getNodeDescription(expected)} is expected`);
+        aIter.next();
+      }
+
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null after calling next() finally (got: ${getNodeDescription(aIter.currentNode)}`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true after calling next() finally`);
+    } else {
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null immediately after initialization (got: ${getNodeDescription(aIter.currentNode)})`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true immediately after initialization`);
+
+      aIter.first();
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null after calling first() (got: ${getNodeDescription(aIter.currentNode)})`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true after calling first()`);
+    }
+  }
+
+  document.body.innerHTML = "<p>" +
+                              "Here is <b>bold</b> and <i><u>underlined and </u>italic </i><span>or no style text.</span><br>" +
+                            "</p>" +
+                            "<p>" +
+                              "Here is an &lt;input&gt; element: <input type=\"text\" value=\"default value\"><br>\n" +
+                              "and a &lt;textarea&gt; element: <textarea>text area's text node</textarea><br><br>\n" +
+                              "<!-- and here is comment node -->" +
+                            "</p>";
+
+  let expectedResult =
+    [document.body, // <body>
+     document.body.firstChild, // first <p>
+     document.body.firstChild.firstChild, // the first text node
+     document.body.firstChild.firstChild.nextSibling, // <b>
+     document.body.firstChild.firstChild.nextSibling.firstChild, // text in <b>
+     document.body.firstChild.firstChild.nextSibling.nextSibling, // text next to <b>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling, // <i>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, // <u>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild.firstChild, // text in <u>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild.nextSibling, // text next to <u>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.nextSibling, // <span>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.firstChild, // text in <span>
+     document.body.firstChild.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // <br> next to <span>
+     document.body.firstChild.nextSibling, // second <p>
+     document.body.firstChild.nextSibling.firstChild, // the first text node in second <p>
+     document.body.firstChild.nextSibling.firstChild.nextSibling, // <input>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling, // <br> next to <input>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling, // text next to <input>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling, // <textarea>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.firstChild, // text in <textarea>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // <br> next to <textarea>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // <br> next to <br>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling, // text next to <br>
+     document.body.firstChild.nextSibling.firstChild.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling.nextSibling] // comment
+
+  iter.initWithRootNode(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, document.body);
+  check(iter, expectedResult, "Initialized with the <body> as root element");
+
+  /**
+   * Selects the <body> with a range.
+   */
+  range = document.createRange();
+  range.selectNode(document.body);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter, expectedResult, "Initialized with range selecting the <body>");
+
+  /**
+   * Selects the <body> with positions.
+   */
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset, range.endContainer, range.endOffset);
+  check(iter, expectedResult, "Initialized with positions selecting the <body>");
+
+  /**
+   * Selects all children in the <body> with a range.
+   */
+  expectedResult.shift(); // <body> shouldn't be listed up.
+  range = document.createRange();
+  range.selectNodeContents(document.body);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter, expectedResult, "Initialized with range selecting all children in the <body>");
+
+  /**
+   * Selects all children in the <body> with positions.
+   */
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset, range.endContainer, range.endOffset);
+  check(iter, expectedResult, "Initialized with positions selecting all children in the <body>");
+
+  /**
+   * range/positions around elements.
+   */
+  document.body.innerHTML = "abc<b>def</b><i>ghi</i>jkl";
+  range = document.createRange();
+
+  range.setStart(document.body.firstChild, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '[abc<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '[abc<b>de]f'");
+
+  range.setStart(document.body.firstChild, 2);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting 'ab[c<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting 'ab[c<b>de]f'");
+
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting 'abc[<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <b>
+         document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting 'abc[<b>de]f'");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting 'abc{<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <b>
+         document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting 'abc{<b>de]f'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{de]f'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{def]</b>'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{def]</b>'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling, 1);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{def}</b>'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{def}</b>'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{def</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{def</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling.firstChild, 3);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>def[</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>def[</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter, [],
+        "Initialized with range selecting '<b>def{</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [],
+        "Initialized with positions selecting '<b>def{</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with range selecting '<b>def{</b><i>}ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling], // <i>
+        "Initialized with positions selecting '<b>def{</b><i>}ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling.firstChild, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling, // <i>
+         document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with range selecting '<b>def{</b><i>]ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling, // <i>
+         document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with positions selecting '<b>def{</b><i>]ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 0);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with range selecting '<i>{ghi</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with positions selecting '<i>{ghi</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling.firstChild, 3);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with range selecting '<i>ghi[</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with positions selecting '<i>ghi[</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 1);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter, [],
+        "Initialized with range selecting '<i>ghi{</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [],
+        "Initialized with positions selecting '<i>ghi{</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.nextSibling], // text after <i>
+        "Initialized with range selecting '<i>ghi{</i>]jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.nextSibling], // text after <i>
+        "Initialized with positions selecting '<i>ghi{</i>]jkl'");
+
+  /**
+   * range/positions around <br> elements.
+   */
+  document.body.innerHTML = "abc<br>def";
+  range = document.createRange();
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // text before <br>
+         document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with range selecting 'abc[<br>]def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // text before <br>
+         document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with positions selecting 'abc[<br>]def'");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with range selecting 'abc{<br>]def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with positions selecting 'abc{<br>]def'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with range selecting 'abc{<br>]def' (starting in <br>)");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling, // <br>
+         document.body.firstChild.nextSibling.nextSibling], // text after <br>
+        "Initialized with positions selecting 'abc{<br>]def' (starting in <br>)");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with range selecting 'abc{<br>}def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with positions selecting 'abc{<br>}def'");
+
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild], // text before <br>
+        "Initialized with range selecting 'abc[}<br>def' (ending in <br>)");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.PRE_ORDER_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild], // text before <br>
+        "Initialized with positions selecting 'abc[}<br>def' (ending in <br>)");
+
+  finish();
+});
+</script>
+</head>
+<body></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_content_iterator_subtree.html
@@ -0,0 +1,690 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for content subtree iterator</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+<script>
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+function finish() {
+  // The SimpleTest may require usual elements in the template, but they shouldn't be during test.
+  // So, let's create them at end of the test.
+  document.body.innerHTML = '<div id="display"></div><div id="content"></div><pre id="test"></pre>';
+  SimpleTest.finish();
+}
+
+function createContentIterator() {
+  return Cc["@mozilla.org/scriptable-content-iterator;1"]
+      .createInstance(Ci.nsIScriptableContentIterator);
+}
+
+function getNodeDescription(aNode) {
+  if (aNode === undefined) {
+    return "undefine";
+  }
+  if (aNode === null) {
+    return "null";
+  }
+  function getElementDescription(aElement) {
+    if (aElement.tagName === "BR") {
+      if (aElement.previousSibling) {
+        return `<br> element after ${getNodeDescription(aElement.previousSibling)}`;
+      }
+      return `<br> element in ${getElementDescription(aElement.parentElement)}`;
+    }
+    let hasHint = aElement == document.body;
+    let tag = `<${aElement.tagName.toLowerCase()}`;
+    if (aElement.getAttribute("id")) {
+      tag += ` id="${aElement.getAttribute("id")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("class")) {
+      tag += ` class="${aElement.getAttribute("class")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("type")) {
+      tag += ` type="${aElement.getAttribute("type")}"`;
+    }
+    if (aElement.getAttribute("name")) {
+      tag += ` name="${aElement.getAttribute("name")}"`;
+    }
+    if (aElement.getAttribute("value")) {
+      tag += ` value="${aElement.getAttribute("value")}"`;
+      hasHint = true;
+    }
+    if (aElement.getAttribute("style")) {
+      tag += ` style="${aElement.getAttribute("style")}"`;
+      hasHint = true;
+    }
+    if (hasHint) {
+      return tag + ">";
+    }
+    return `${tag}> in ${getElementDescription(aElement.parentElement)}`;
+  }
+  switch (aNode.nodeType) {
+    case aNode.TEXT_NODE:
+      return `text node, "${aNode.wholeText.replace(/\n/g, '\\n')}"`;
+    case aNode.COMMENT_NODE:
+      return `comment node, "${aNode.data.replace(/\n/g, '\\n')}"`;
+    case aNode.ELEMENT_NODE:
+      return getElementDescription(SpecialPowers.unwrap(aNode));
+    default:
+      return "unknown node";
+  }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function () {
+  let iter = createContentIterator();
+
+  /**
+   * FYI: nsContentSubtreeIterator does not support initWithRootNode() nor positionAt().
+   */
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with a range which selects empty element.
+   */
+  document.body.innerHTML = "<div></div>";
+  let range = document.createRange();
+  range.selectNode(document.body.firstChild);
+  let description = "Initialized with range including only empty <div>:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with positions which select empty element.
+   */
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with positions including only empty <div>:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Tests to initializing with collapsed range in an empty element.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild, 0);
+  description = "Initialized with range collapsed in empty <div>:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range in an empty element.
+   */
+  description = "Initialized with a position in empty <div>:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         document.body.firstChild, 0, document.body.firstChild, 0);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Basic behavior tests of first(), last(), prev() and next() after initialized with a range which selects the text node.
+   */
+  document.body.innerHTML = "<div>some text.</div>";
+  range = document.createRange();
+  range.selectNode(document.body.firstChild.firstChild);
+  description = "Initialized with range including only text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.last();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling last() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling last()`);
+
+  iter.prev();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling prev() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling prev()`); // XXX Is this expected?
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild.firstChild,
+    `${description} currentNode should be the text node after calling first() even after once done (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first() even after once done`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next()`);
+
+  /**
+   * Basic behavior tests of first() and next() after initialized with positions which select the text node.
+   * XXX In this case, content iterator lists up the parent <div> element.  Not sure if this is intentional difference
+   *     from initWithRange().
+   */
+  range.selectNode(document.body.firstChild);
+  description = "Initialized with positions including only text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> element immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), document.body.firstChild,
+    `${description} currentNode should be the <div> element after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(!iter.isDone, `${description} isDone shouldn't be true after calling first()`);
+
+  iter.next();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null after calling next() from first position (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true after calling next() from first position`);
+
+  /**
+   * Tests to initializing with collapsed range at start of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, 0);
+  description = "Initialized with range collapsed at start of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at start of a text node.
+   */
+  description = "Initialized with a position at start of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         document.body.firstChild.firstChild, 0, document.body.firstChild.firstChild, 0);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at end of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  description = "Initialized with range collapsed at end of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at end of a text node.
+   */
+  description = "Initialized with a position at end of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at middle of a text node.
+   */
+  range = document.createRange();
+  range.collapse(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2);
+  description = "Initialized with range collapsed at end of text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with collapsed range at middle of a text node.
+   */
+  description = "Initialized with a position at end of text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length / 2);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with a range selecting all text in a text node.
+   */
+  range = document.createRange();
+  range.setStart(document.body.firstChild.firstChild, 0);
+  range.setEnd(document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  description = "Initialized with range selecting all text in text node:";
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Tests to initializing with positions selecting all text in a text node.
+   */
+  description = "Initialized with positions selecting all text in text node:";
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         document.body.firstChild.firstChild, 0,
+                         document.body.firstChild.firstChild, document.body.firstChild.firstChild.length);
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null immediately after initialization (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true immediately after initialization`);
+
+  iter.first();
+  is(SpecialPowers.unwrap(iter.currentNode), null,
+    `${description} currentNode should be null even after calling first() (got: ${getNodeDescription(iter.currentNode)})`);
+  ok(iter.isDone, `${description} isDone should be true even after calling first()`);
+
+  /**
+   * Basic tests with complicated tree.
+   */
+  function check(aIter, aExpectedResult, aDescription) {
+    if (aExpectedResult.length > 0) {
+      is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+        `${aDescription}: currentNode should be the text node immediately after initialization (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+      ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true immediately after initialization`);
+
+      aIter.first();
+      is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0],
+        `${aDescription}: currentNode should be the text node after calling first() (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`);
+      ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true after calling first()`);
+
+      for (let expected of aExpectedResult) {
+        is(SpecialPowers.unwrap(aIter.currentNode), expected,
+          `${aDescription}: currentNode should be the node (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(expected)})`);
+        ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true when ${getNodeDescription(expected)} is expected`);
+        aIter.next();
+      }
+
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null after calling next() finally (got: ${getNodeDescription(aIter.currentNode)}`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true after calling next() finally`);
+    } else {
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null immediately after initialization (got: ${getNodeDescription(aIter.currentNode)})`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true immediately after initialization`);
+
+      aIter.first();
+      is(SpecialPowers.unwrap(aIter.currentNode), null,
+        `${aDescription}: currentNode should be null after calling first() (got: ${getNodeDescription(aIter.currentNode)})`);
+      ok(aIter.isDone, `${aDescription}: isDone should be true after calling first()`);
+    }
+  }
+
+  document.body.innerHTML = "<p>" +
+                              "Here is <b>bold</b> and <i><u>underlined and </u>italic </i><span>or no style text.</span><br>" +
+                            "</p>" +
+                            "<p>" +
+                              "Here is an &lt;input&gt; element: <input type=\"text\" value=\"default value\"><br>\n" +
+                              "and a &lt;textarea&gt; element: <textarea>text area's text node</textarea><br><br>\n" +
+                              "<!-- and here is comment node -->" +
+                            "</p>";
+
+  /**
+   * Selects the <body> with a range.
+   */
+  range = document.createRange();
+  range.selectNode(document.body);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [document.body], "Initialized with range selecting the <body>");
+
+  /**
+   * Selects the <body> with positions.
+   */
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset, range.endContainer, range.endOffset);
+  check(iter, [document.body], "Initialized with positions selecting the <body>");
+
+  /**
+   * Selects all children in the <body> with a range.
+   */
+  range = document.createRange();
+  range.selectNodeContents(document.body);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild, // first <p>
+         document.body.firstChild.nextSibling], // second <p>
+        "Initialized with range selecting all children in the <body>");
+
+  /**
+   * Selects all children in the <body> with positions.
+   */
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset, range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild, // first <p>
+         document.body.firstChild.nextSibling], // second <p>
+        "Initialized with positions selecting all children in the <body>");
+
+  /**
+   * range/positions around elements.
+   */
+  document.body.innerHTML = "abc<b>def</b><i>ghi</i>jkl";
+  range = document.createRange();
+
+  range.setStart(document.body.firstChild, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '[abc<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '[abc<b>de]f'");
+
+  range.setStart(document.body.firstChild, 2);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,[], "Initialized with range selecting 'ab[c<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting 'ab[c<b>de]f'");
+
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting 'abc[<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting 'abc[<b>de]f'");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting 'abc{<b>de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting 'abc{<b>de]f'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<b>{de]f'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<b>{de]f'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.firstChild, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<b>{def]</b>'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<b>{def]</b>'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling, 1);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with range selecting '<b>{def}</b>'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{def}</b>'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+       "Initialized with range selecting '<b>{def</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.firstChild], // text in <b>
+        "Initialized with positions selecting '<b>{def</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling.firstChild, 3);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<b>def[</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<b>def[</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<b>def{</b>}<i>ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<b>def{</b>}<i>ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<b>def{</b><i>}ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<b>def{</b><i>}ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling.firstChild, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<b>def{</b><i>]ghi'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<b>def{</b><i>]ghi'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 0);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with range selecting '<i>{ghi</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling.nextSibling.firstChild], // text in <i>
+        "Initialized with positions selecting '<i>{ghi</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling.firstChild, 3);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<i>ghi[</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<i>ghi[</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 1);
+  range.setEnd(document.body, 3);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<i>ghi{</i>}jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<i>ghi{</i>}jkl'");
+
+  range.setStart(document.body.firstChild.nextSibling.nextSibling, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting '<i>ghi{</i>]jkl'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting '<i>ghi{</i>]jkl'");
+
+  /**
+   * range/positions around <br> elements.
+   */
+  document.body.innerHTML = "abc<br>def";
+  range = document.createRange();
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with range selecting 'abc[<br>]def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with positions selecting 'abc[<br>]def'");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with range selecting 'abc{<br>]def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with positions selecting 'abc{<br>]def'");
+
+  range.setStart(document.body.firstChild.nextSibling, 0);
+  range.setEnd(document.body.firstChild.nextSibling.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting 'abc{<br>]def' (starting in <br>)");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting 'abc{<br>]def' (starting in <br>)");
+
+  range.setStart(document.body, 1);
+  range.setEnd(document.body, 2);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with range selecting 'abc{<br>}def'");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter,
+        [document.body.firstChild.nextSibling], // <br>
+        "Initialized with positions selecting 'abc{<br>}def'");
+
+  range.setStart(document.body.firstChild, 3);
+  range.setEnd(document.body.firstChild.nextSibling, 0);
+  iter.initWithRange(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range);
+  check(iter, [], "Initialized with range selecting 'abc[}<br>def' (ending in <br>)");
+  iter.initWithPositions(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR,
+                         range.startContainer, range.startOffset,
+                         range.endContainer, range.endOffset);
+  check(iter, [], "Initialized with positions selecting 'abc[}<br>def' (ending in <br>)");
+
+  finish();
+});
+</script>
+</head>
+<body></body>
+</html>
--- a/dom/events/InputEvent.cpp
+++ b/dom/events/InputEvent.cpp
@@ -22,29 +22,44 @@ InputEvent::InputEvent(EventTarget* aOwn
   if (aEvent) {
     mEventIsInternal = false;
   } else {
     mEventIsInternal = true;
     mEvent->mTime = PR_Now();
   }
 }
 
+void InputEvent::GetInputType(nsAString& aInputType) {
+  InternalEditorInputEvent* editorInputEvent = mEvent->AsEditorInputEvent();
+  MOZ_ASSERT(editorInputEvent);
+  if (editorInputEvent->mInputType == EditorInputType::eUnknown) {
+    aInputType = mInputTypeValue;
+  } else {
+    editorInputEvent->GetDOMInputTypeName(aInputType);
+  }
+}
+
 bool InputEvent::IsComposing() {
   return mEvent->AsEditorInputEvent()->mIsComposing;
 }
 
 already_AddRefed<InputEvent> InputEvent::Constructor(
     const GlobalObject& aGlobal, const nsAString& aType,
     const InputEventInit& aParam, ErrorResult& aRv) {
   nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports());
   RefPtr<InputEvent> e = new InputEvent(t, nullptr, nullptr);
   bool trusted = e->Init(t);
   e->InitUIEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView,
                  aParam.mDetail);
   InternalEditorInputEvent* internalEvent = e->mEvent->AsEditorInputEvent();
+  internalEvent->mInputType =
+      InternalEditorInputEvent::GetEditorInputType(aParam.mInputType);
+  if (internalEvent->mInputType == EditorInputType::eUnknown) {
+    e->mInputTypeValue = aParam.mInputType;
+  }
   internalEvent->mIsComposing = aParam.mIsComposing;
   e->SetTrusted(trusted);
   e->SetComposed(aParam.mComposed);
   return e.forget();
 }
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/events/InputEvent.h
+++ b/dom/events/InputEvent.h
@@ -26,20 +26,25 @@ class InputEvent : public UIEvent {
                                                   const InputEventInit& aParam,
                                                   ErrorResult& aRv);
 
   virtual JSObject* WrapObjectInternal(
       JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override {
     return InputEvent_Binding::Wrap(aCx, this, aGivenProto);
   }
 
+  void GetInputType(nsAString& aInputType);
   bool IsComposing();
 
  protected:
   ~InputEvent() {}
+
+  // mInputTypeValue stores inputType attribute value if the instance is
+  // created by script and not initialized with known inputType value.
+  nsString mInputTypeValue;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 already_AddRefed<mozilla::dom::InputEvent> NS_NewDOMInputEvent(
     mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext,
     mozilla::InternalEditorInputEvent* aEvent);
new file mode 100644
--- /dev/null
+++ b/dom/events/InputTypeList.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This header file defines all inputType values which are used for DOM
+ * InputEvent.inputType.
+ * You must define NS_DEFINE_INPUTTYPE macro before including this.
+ *
+ * It must have two arguments, (aCPPName, aDOMName)
+ * aCPPName is usable name for a part of C++ constants.
+ * aDOMName is the actual value declared by the specs:
+ * Level 1:
+ *   https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
+ * Level 2:
+ *   https://w3c.github.io/input-events/index.html#interface-InputEvent-Attributes
+ */
+
+NS_DEFINE_INPUTTYPE(InsertText, "insertText")
+NS_DEFINE_INPUTTYPE(InsertReplacementText, "insertReplacementText")
+NS_DEFINE_INPUTTYPE(InsertLineBreak, "insertLineBreak")
+NS_DEFINE_INPUTTYPE(InsertParagraph, "insertParagraph")
+NS_DEFINE_INPUTTYPE(InsertOrderedList, "insertOrderedList")
+NS_DEFINE_INPUTTYPE(InsertUnorderedList, "insertUnorderedList")
+NS_DEFINE_INPUTTYPE(InsertHorizontalRule, "insertHorizontalRule")
+NS_DEFINE_INPUTTYPE(InsertFromYank, "insertFromYank")
+NS_DEFINE_INPUTTYPE(InsertFromDrop, "insertFromDrop")
+NS_DEFINE_INPUTTYPE(InsertFromPaste, "insertFromPaste")
+NS_DEFINE_INPUTTYPE(InsertTranspose, "insertTranspose")
+NS_DEFINE_INPUTTYPE(InsertCompositionText, "insertCompositionText")
+NS_DEFINE_INPUTTYPE(InsertFromComposition,
+                    "insertFromComposition")  // Level 2
+NS_DEFINE_INPUTTYPE(InsertLink, "insertLink")
+NS_DEFINE_INPUTTYPE(DeleteByComposition,
+                    "deleteByComposition")  // Level 2
+NS_DEFINE_INPUTTYPE(DeleteCompositionText,
+                    "deleteCompositionText")  // Level 2
+NS_DEFINE_INPUTTYPE(DeleteWordBackward, "deleteWordBackward")
+NS_DEFINE_INPUTTYPE(DeleteWordForward, "deleteWordForward")
+NS_DEFINE_INPUTTYPE(DeleteSoftLineBackward, "deleteSoftLineBackward")
+NS_DEFINE_INPUTTYPE(DeleteSoftLineForward, "deleteSoftLineForward")
+NS_DEFINE_INPUTTYPE(DeleteEntireSoftLine, "deleteEntireSoftLine")
+NS_DEFINE_INPUTTYPE(DeleteHardLineBackward, "deleteHardLineBackward")
+NS_DEFINE_INPUTTYPE(DeleteHardLineForward, "deleteHardLineForward")
+NS_DEFINE_INPUTTYPE(DeleteByDrag, "deleteByDrag")
+NS_DEFINE_INPUTTYPE(DeleteByCut, "deleteByCut")
+NS_DEFINE_INPUTTYPE(DeleteContent, "deleteContent")
+NS_DEFINE_INPUTTYPE(DeleteContentBackward, "deleteContentBackward")
+NS_DEFINE_INPUTTYPE(DeleteContentForward, "deleteContentForward")
+NS_DEFINE_INPUTTYPE(HistoryUndo, "historyUndo")
+NS_DEFINE_INPUTTYPE(HistoryRedo, "historyRedo")
+NS_DEFINE_INPUTTYPE(FormatBold, "formatBold")
+NS_DEFINE_INPUTTYPE(FormatItalic, "formatItalic")
+NS_DEFINE_INPUTTYPE(FormatUnderline, "formatUnderline")
+NS_DEFINE_INPUTTYPE(FormatStrikeThrough, "formatStrikeThrough")
+NS_DEFINE_INPUTTYPE(FormatSuperscript, "formatSuperscript")
+NS_DEFINE_INPUTTYPE(FormatSubscript, "formatSubscript")
+NS_DEFINE_INPUTTYPE(FormatJustifyFull, "formatJustifyFull")
+NS_DEFINE_INPUTTYPE(FormatJustifyCenter, "formatJustifyCenter")
+NS_DEFINE_INPUTTYPE(FormatJustifyRight, "formatJustifyRight")
+NS_DEFINE_INPUTTYPE(FormatJustifyLeft, "formatJustifyLeft")
+NS_DEFINE_INPUTTYPE(FormatIndent, "formatIndent")
+NS_DEFINE_INPUTTYPE(FormatOutdent, "formatOutdent")
+NS_DEFINE_INPUTTYPE(FormatRemove, "formatRemove")
+NS_DEFINE_INPUTTYPE(FormatSetBlockTextDirection, "formatSetBlockTextDirection")
+NS_DEFINE_INPUTTYPE(FormatSetInlineTextDirection,
+                    "formatSetInlineTextDirection")
+NS_DEFINE_INPUTTYPE(FormatBackColor, "formatBackColor")
+NS_DEFINE_INPUTTYPE(FormatFontColor, "formatFontColor")
+NS_DEFINE_INPUTTYPE(FormatFontName, "formatFontName")
--- a/dom/events/moz.build
+++ b/dom/events/moz.build
@@ -30,16 +30,17 @@ EXPORTS.mozilla += [
     'DOMEventTargetHelper.h',
     'EventDispatcher.h',
     'EventListenerManager.h',
     'EventNameList.h',
     'EventStateManager.h',
     'EventStates.h',
     'IMEContentObserver.h',
     'IMEStateManager.h',
+    'InputTypeList.h',
     'InternalMutationEvent.h',
     'JSEventHandler.h',
     'KeyNameList.h',
     'PendingFullscreenEvent.h',
     'PhysicalKeyCodeNameList.h',
     'TextComposition.h',
     'VirtualKeyCodeList.h',
     'WheelHandlingHelper.h',
--- a/dom/events/test/test_eventctors.html
+++ b/dom/events/test/test_eventctors.html
@@ -880,12 +880,31 @@ is(e.animationName, "bounce3", "Animatio
 is(e.elapsedTime, 3.5, "Animation event copies elapsedTime from AnimationEventInit");
 is(e.pseudoElement, "", "Animation event copies pseudoElement from AnimationEventInit");
 is(e.bubbles, false, "Lack of bubbles property in AnimationEventInit");
 is(e.cancelable, false, "Lack of cancelable property in AnimationEventInit");
 is(e.type, "hello", "Wrong event type!");
 is(e.isTrusted, false, "Event shouldn't be trusted!");
 is(e.eventPhase, Event.NONE, "Wrong event phase");
 
+// InputEvent
+e = new InputEvent("hello", {data: "something data", inputType: "invalid input type", isComposing: true});
+is(e.type, "hello", "InputEvent should set type attribute");
+todo_is(e.data, "something data", "InputEvent should have data attribute");
+is(e.inputType, "invalid input type", "InputEvent should have inputType attribute");
+is(e.isComposing, true, "InputEvent should have isComposing attribute");
+
+e = new InputEvent("hello", {inputType: "insertText"});
+is(e.inputType, "insertText", "InputEvent.inputType should return valid inputType from EditorInputType enum");
+e = new InputEvent("hello", {inputType: "deleteWordBackward"});
+is(e.inputType, "deleteWordBackward", "InputEvent.inputType should return valid inputType from EditorInputType enum");
+e = new InputEvent("hello", {inputType: "formatFontName"});
+is(e.inputType, "formatFontName", "InputEvent.inputType should return valid inputType from EditorInputType enum");
+
+e = new InputEvent("input", {});
+todo_is(e.data, "", "InputEvent.data should be empty string in default");
+is(e.inputType, "", "InputEvent.inputType should be empty string in default");
+is(e.isComposing, false, "InputEvent.isComposing should be false in default");
+
 </script>
 </pre>
 </body>
 </html>
--- a/dom/html/nsTextEditorState.cpp
+++ b/dom/html/nsTextEditorState.cpp
@@ -2421,23 +2421,25 @@ bool nsTextEditorState::SetValue(const n
       }
 
       // Update the frame display if needed
       if (mBoundFrame) {
         mBoundFrame->UpdateValueDisplay(true);
       }
 
       // If this is called as part of user input, we need to dispatch "input"
-      // event since web apps may want to know the user operation.
+      // event with "insertReplacementText" since web apps may want to know
+      // the user operation which changes editor value with a built-in function
+      // like autocomplete, password manager, session restore, etc.
       if (aFlags & eSetValue_BySetUserInput) {
         nsCOMPtr<Element> element = do_QueryInterface(textControlElement);
         MOZ_ASSERT(element);
-        RefPtr<TextEditor> textEditor;
-        DebugOnly<nsresult> rvIgnored =
-            nsContentUtils::DispatchInputEvent(element, textEditor);
+        RefPtr<TextEditor> textEditor;  // See bug 1506439
+        DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
+            element, EditorInputType::eInsertReplacementText, textEditor);
         NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                              "Failed to dispatch input event");
       }
     } else {
       // Even if our value is not actually changing, apparently we need to mark
       // our SelectionProperties dirty to make accessibility tests happy.
       // Probably because they depend on the SetSelectionRange() call we make on
       // our frame in RestoreSelectionState, but I have no idea why they do.
--- a/dom/html/test/forms/test_MozEditableElement_setUserInput.html
+++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html
@@ -157,16 +157,18 @@ SimpleTest.waitForFocus(() => {
     if (inputEvents.length > 0) {
       if (SpecialPowers.wrap(target).isInputEventTarget) {
         if (test.type === "number" || test.type === "time") {
           todo(inputEvents[0] instanceof InputEvent,
                `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
         } else {
           ok(inputEvents[0] instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
+          is(inputEvents[0].inputType, "insertReplacementText",
+             `inputType should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
         }
       } else {
         ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
            `"input" event should be dispatched with Event interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`);
       }
       is(inputEvents[0].cancelable, false,
          `"input" event should be never cancelable (${tag}, before getting focus)`);
       is(inputEvents[0].bubbles, true,
@@ -213,16 +215,18 @@ SimpleTest.waitForFocus(() => {
     if (inputEvents.length > 0) {
       if (SpecialPowers.wrap(target).isInputEventTarget) {
         if (test.type === "number" || test.type === "time") {
           todo(inputEvents[0] instanceof InputEvent,
                `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
         } else {
           ok(inputEvents[0] instanceof InputEvent,
              `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
+          is(inputEvents[0].inputType, "insertReplacementText",
+             `inputType should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
         }
       } else {
         ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent),
            `"input" event should be dispatched with Event interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`);
       }
       is(inputEvents[0].cancelable, false,
          `"input" event should be never cancelable (${tag}, after getting focus)`);
       is(inputEvents[0].bubbles, true,
--- a/dom/html/test/forms/test_input_event.html
+++ b/dom/html/test/forms/test_input_event.html
@@ -35,25 +35,28 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
   /** Test for input event. This is highly based on test_change_event.html **/
 
   const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent);
 
+  let expectedInputType = "";
   function checkIfInputIsInputEvent(aEvent, aToDo, aDescription) {
     if (aToDo) {
       // Probably, key operation should fire "input" event with InputEvent interface.
       // See https://github.com/w3c/input-events/issues/88
       todo(aEvent instanceof InputEvent,
          `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     } else {
       ok(aEvent instanceof InputEvent,
          `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+      is(aEvent.inputType, expectedInputType,
+         `inputType should be "${expectedInputType}" ${aDescription}`);
     }
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
   }
 
   function checkIfInputIsEvent(aEvent, aDescription) {
@@ -123,69 +126,79 @@ https://bugzilla.mozilla.org/show_bug.cg
     setTimeout(testUserInput2, 0);
   }
 
   function testUserInput2() {
     // Some generic checks for types that support the input event.
     for (var i = 0; i < textTypes.length; ++i) {
       input = document.getElementById("input_" + textTypes[i]);
       input.focus();
+      expectedInputType = "insertLineBreak";
       synthesizeKey("KEY_Enter");
       is(textInput[i], 0, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
 
+      expectedInputType = "insertText";
       sendString("m");
       is(textInput[i], 1, textTypes[i] + " input element should have dispatched input event.");
+      expectedInputType = "insertLineBreak";
       synthesizeKey("KEY_Enter");
       is(textInput[i], 1, "input event shouldn't be dispatched on " + textTypes[i] + " input element");
 
+      expectedInputType = "deleteContentBackward";
       synthesizeKey("KEY_Backspace");
       is(textInput[i], 2, textTypes[i] + " input element should have dispatched input event.");
     }
 
     // Some scenarios of value changing from script and from user input.
     input = document.getElementById("input_text");
     input.focus();
+    expectedInputType = "insertText";
     sendString("f");
     is(textInput[0], 3, "input event should have been dispatched");
     input.blur();
     is(textInput[0], 3, "input event should not have been dispatched");
 
     input.focus();
+    expectedInputType = "insertText";
     input.value = 'foo';
     is(textInput[0], 3, "input event should not have been dispatched");
     input.blur();
     is(textInput[0], 3, "input event should not have been dispatched");
 
     input.focus();
+    expectedInputType = "insertText";
     sendString("f");
     is(textInput[0], 4, "input event should have been dispatched");
     input.value = 'bar';
     is(textInput[0], 4, "input event should not have been dispatched");
     input.blur();
     is(textInput[0], 4, "input event should not have been dispatched");
 
     // Same for textarea.
     var textarea = document.getElementById("textarea");
     textarea.focus();
+    expectedInputType = "insertText";
     sendString("f");
     is(textareaInput, 1, "input event should have been dispatched");
     textarea.blur();
     is(textareaInput, 1, "input event should not have been dispatched");
 
     textarea.focus();
     textarea.value = 'foo';
     is(textareaInput, 1, "input event should not have been dispatched");
     textarea.blur();
     is(textareaInput, 1, "input event should not have been dispatched");
 
     textarea.focus();
+    expectedInputType = "insertText";
     sendString("f");
     is(textareaInput, 2, "input event should have been dispatched");
     textarea.value = 'bar';
     is(textareaInput, 2, "input event should not have been dispatched");
+    expectedInputType = "deleteContentBackward";
     synthesizeKey("KEY_Backspace");
     is(textareaInput, 3, "input event should have been dispatched");
     textarea.blur();
     is(textareaInput, 3, "input event should not have been dispatched");
 
     // Non-text input tests:
     for (var i = 0; i < NonTextTypes.length; ++i) {
       // Button, submit, image and reset input type tests.
@@ -256,16 +269,19 @@ https://bugzilla.mozilla.org/show_bug.cg
     // Tests for type='number'.
     // We only test key events here since input events for mouse event changes
     // are tested in test_input_number_mouse_events.html
     var number = document.getElementById("input_number");
 
     if (isDesktop) { // up/down arrow keys not supported on android/b2g
       number.value = "";
       number.focus();
+      // <input type="number">'s inputType value hasn't been decided, see
+      // https://github.com/w3c/input-events/issues/88
+      expectedInputType = "";
       synthesizeKey("KEY_ArrowUp");
       is(numberInput, 1, "input event should be dispatched for up/down arrow key keypress");
       is(number.value, "1", "sanity check value of number control after keypress");
 
       synthesizeKey("KEY_ArrowDown", {repeat: 3});
       is(numberInput, 4, "input event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated");
       is(number.value, "-2", "sanity check value of number control after multiple keydown events");
 
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -565,27 +565,25 @@ nsresult MediaEngineWebRTCMicrophoneSour
     // because we can only have one MSG per document.
     return NS_ERROR_FAILURE;
   }
 
   mInputProcessing = new AudioInputProcessing(mDeviceMaxChannelCount, mStream,
                                               mTrackID, mPrincipal);
 
   RefPtr<MediaEngineWebRTCMicrophoneSource> that = this;
-  RefPtr<MediaStreamGraphImpl> gripGraph = mStream->GraphImpl();
-  NS_DispatchToMainThread(
-      media::NewRunnableFrom([that, graph = std::move(gripGraph), deviceID,
-                              stream = mStream, track = mTrackID]() {
-        if (graph) {
+  NS_DispatchToMainThread(media::NewRunnableFrom(
+      [that, deviceID, stream = mStream, track = mTrackID]() {
+        if (MediaStreamGraphImpl* graph = stream->GraphImpl()) {
           graph->AppendMessage(MakeUnique<StartStopMessage>(
               that->mInputProcessing, StartStopMessage::Start));
+          stream->SetPullingEnabled(track, true);
         }
 
         stream->OpenAudioInput(deviceID, that->mInputProcessing);
-        stream->SetPullingEnabled(track, true);
 
         return NS_OK;
       }));
 
   ApplySettings(mCurrentPrefs);
 
   MOZ_ASSERT(mState != kReleased);
   mState = kStarted;
@@ -601,29 +599,27 @@ nsresult MediaEngineWebRTCMicrophoneSour
   MOZ_ASSERT(mStream, "SetTrack must have been called before ::Stop");
 
   if (mState == kStopped) {
     // Already stopped - this is allowed
     return NS_OK;
   }
 
   RefPtr<MediaEngineWebRTCMicrophoneSource> that = this;
-  RefPtr<MediaStreamGraphImpl> gripGraph = mStream->GraphImpl();
   NS_DispatchToMainThread(
-      media::NewRunnableFrom([that, graph = std::move(gripGraph),
-                              stream = mStream, track = mTrackID]() {
-        if (graph) {
+      media::NewRunnableFrom([that, stream = mStream, track = mTrackID]() {
+        if (MediaStreamGraphImpl* graph = stream->GraphImpl()) {
+          stream->SetPullingEnabled(track, false);
           graph->AppendMessage(MakeUnique<StartStopMessage>(
               that->mInputProcessing, StartStopMessage::Stop));
         }
 
         CubebUtils::AudioDeviceID deviceID = that->mDeviceInfo->DeviceID();
         Maybe<CubebUtils::AudioDeviceID> id = Some(deviceID);
         stream->CloseAudioInput(id, that->mInputProcessing);
-        stream->SetPullingEnabled(track, false);
 
         return NS_OK;
       }));
 
   MOZ_ASSERT(mState == kStarted, "Should be started when stopping");
   mState = kStopped;
 
   return NS_OK;
--- a/dom/tests/mochitest/general/test_clipboard_events.html
+++ b/dom/tests/mochitest/general/test_clipboard_events.html
@@ -188,127 +188,162 @@ add_task(async function test_input_oncop
   // Setup an oncopy event handler, fire copy.  Ensure that the event
   // handler was called, and the clipboard contents have been set to 'PUT TE',
   // which is the part that is selected below.
   selectContentInput();
   contentInput.focus();
   contentInput.setSelectionRange(2, 8);
 
   var oncopy_fired = false;
+  var oninput_fired = false;
   contentInput.oncopy = function() { oncopy_fired = true; };
+  contentInput.oninput = function () { oninput_fired = true; };
   try {
     await putOnClipboard("PUT TE", () => {
       synthesizeKey("c", {accelKey: 1});
     }, "copy on plaintext editor set clipboard correctly");
     ok(oncopy_fired, "copy event firing on plaintext editor");
+    ok(!oninput_fired, "input event shouldn't be fired on plaintext editor by copy");
   } finally {
     contentInput.oncopy = null;
+    contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_oncut() {
   await reset();
 
   // Setup an oncut event handler, and fire cut.  Ensure that the event
   // handler was fired, the clipboard contains the INPUT TEXT, and
   // that the input itself is empty.
   selectContentInput();
   var oncut_fired = false;
+  var oninput_count = 0;
+  var inputType = "";
   contentInput.oncut = function() { oncut_fired = true; };
+  contentInput.oninput = function (aEvent) {
+    oninput_count++;
+    inputType = aEvent.inputType;
+  };
   try {
     await putOnClipboard("INPUT TEXT", () => {
       synthesizeKey("x", {accelKey: 1});
     }, "cut on plaintext editor set clipboard correctly");
     ok(oncut_fired, "cut event firing on plaintext editor");
+    is(oninput_count, 1, "input event should be fired once by cut");
+    is(inputType, "deleteByCut", "inputType of the input event should be \"deleteByCut\"");
     is(contentInput.value, "",
       "cut on plaintext editor emptied editor");
   } finally {
     contentInput.oncut = null;
+    contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_onpaste() {
   await reset();
 
   // Setup an onpaste event handler, and fire paste.  Ensure that the event
   // handler was fired, the clipboard contents didn't change, and that the
   // input value did change (ie. paste succeeded).
   selectContentInput();
   var onpaste_fired = false;
+  var oninput_count = 0;
+  var inputType = "";
   contentInput.onpaste = function() { onpaste_fired = true; };
+  contentInput.oninput = function(aEvent) {
+    oninput_count++;
+    inputType = aEvent.inputType;
+  };
+
   try {
     synthesizeKey("v", {accelKey: 1});
     ok(onpaste_fired, "paste event firing on plaintext editor");
     is(getClipboardText(), clipboardInitialValue,
       "paste on plaintext editor did not modify clipboard contents");
+    is(oninput_count, 1, "input event should be fired once by cut");
+    is(inputType, "insertFromPaste", "inputType of the input event should be \"insertFromPaste\"");
     is(contentInput.value, clipboardInitialValue,
       "paste on plaintext editor did modify editor value");
   } finally {
     contentInput.onpaste = null;
+    contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_oncopy_abort() {
   await reset();
 
   // Setup an oncopy event handler, fire copy.  Ensure that the event
   // handler was called, and that the clipboard value did NOT change.
   selectContentInput();
   var oncopy_fired = false;
   contentInput.oncopy = function() { oncopy_fired = true; return false; };
+  contentInput.oninput = function() {
+    ok(false, "input event shouldn't be fired by copy but canceled");
+  };
   try {
     await wontPutOnClipboard(clipboardInitialValue, () => {
       synthesizeKey("c", {accelKey: 1});
     }, "aborted copy on plaintext editor did not modify clipboard");
     ok(oncopy_fired, "copy event (to-be-cancelled) firing on plaintext editor");
   } finally {
     contentInput.oncopy = null;
+    contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_oncut_abort() {
   await reset();
 
   // Setup an oncut event handler, and fire cut.  Ensure that the event
   // handler was fired, the clipboard contains the INPUT TEXT, and
   // that the input itself is empty.
   selectContentInput();
   var oncut_fired = false;
   contentInput.oncut = function() { oncut_fired = true; return false; };
+  contentInput.oninput = function() {
+    ok(false, "input event shouldn't be fired by cut but canceled");
+  };
   try {
     await wontPutOnClipboard(clipboardInitialValue, () => {
       synthesizeKey("x", {accelKey: 1});
     }, "aborted cut on plaintext editor did not modify clipboard");
     ok(oncut_fired, "cut event (to-be-cancelled) firing on plaintext editor");
     is(contentInput.value, "INPUT TEXT",
       "aborted cut on plaintext editor did not modify editor contents");
   } finally {
     contentInput.oncut = null;
+    contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_onpaste_abort() {
   await reset();
 
   // Setup an onpaste event handler, and fire paste.  Ensure that the event
   // handler was fired, the clipboard contents didn't change, and that the
   // input value did change (ie. paste succeeded).
   selectContentInput();
   var onpaste_fired = false;
   contentInput.onpaste = function() { onpaste_fired = true; return false; };
+  contentInput.oninput = function() {
+    ok(false, "input event shouldn't be fired by paste but canceled");
+  };
   try {
     synthesizeKey("v", {accelKey: 1});
     ok(onpaste_fired,
       "paste event (to-be-cancelled) firing on plaintext editor");
     is(getClipboardText(), clipboardInitialValue,
       "aborted paste on plaintext editor did not modify clipboard");
     is(contentInput.value, "INPUT TEXT",
       "aborted paste on plaintext editor did not modify modified editor value");
   } finally {
     contentInput.onpaste = null;
+    contentInput.oninput = null;
   }
 });
 
 add_task(async function test_input_cut_dataTransfer() {
   await reset();
 
   // Cut using event.dataTransfer. The event is not cancelled so the default
   // cut should occur
--- a/dom/webidl/InputEvent.webidl
+++ b/dom/webidl/InputEvent.webidl
@@ -3,14 +3,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/.
  */
 
 [Constructor(DOMString type, optional InputEventInit eventInitDict)]
 interface InputEvent : UIEvent
 {
   readonly attribute boolean       isComposing;
+
+  [Pref="dom.inputevent.inputtype.enabled"]
+  readonly attribute DOMString inputType;
 };
 
 dictionary InputEventInit : UIEventInit
 {
   boolean isComposing = false;
+  DOMString inputType = "";
 };
--- a/editor/libeditor/EditAction.h
+++ b/editor/libeditor/EditAction.h
@@ -1,16 +1,19 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_EditAction_h
 #define mozilla_EditAction_h
 
+#include "mozilla/EventForwards.h"
+#include "mozilla/StaticPrefs.h"
+
 namespace mozilla {
 
 /**
  * EditAction indicates which operation or command causes running the methods
  * of editors.
  */
 enum class EditAction {
   // eNone indicates no edit action is being handled.
@@ -59,16 +62,20 @@ enum class EditAction {
   // This may be set even when Selection is not collapsed.
   eDeleteToBeginningOfSoftLine,
 
   // eDeleteToEndOfSoftLine indicates to remove characters between caret and
   // next visual line break.
   // This may be set even when Selection is not collapsed.
   eDeleteToEndOfSoftLine,
 
+  // eDeleteByDrag indicates to remove selection by dragging the content
+  // to different place.
+  eDeleteByDrag,
+
   // eStartComposition indicates that user starts composition.
   eStartComposition,
 
   // eUpdateComposition indicates that user updates composition with
   // new non-empty composition string and IME selections.
   eUpdateComposition,
 
   // eCommitComposition indicates that user commits composition.
@@ -458,15 +465,133 @@ enum class EditSubAction : int32_t {
   // z-index value.
   eDecreaseZIndex,
   eIncreaseZIndex,
 
   // eCreateBogusNode indicates to create a bogus <br> node.
   eCreateBogusNode,
 };
 
+inline EditorInputType ToInputType(EditAction aEditAction) {
+  switch (aEditAction) {
+    case EditAction::eInsertText:
+      return EditorInputType::eInsertText;
+    case EditAction::eReplaceText:
+      return EditorInputType::eInsertReplacementText;
+    case EditAction::eInsertLineBreak:
+      return EditorInputType::eInsertLineBreak;
+    case EditAction::eInsertParagraphSeparator:
+      return EditorInputType::eInsertParagraph;
+    case EditAction::eInsertOrderedListElement:
+    case EditAction::eRemoveOrderedListElement:
+      return EditorInputType::eInsertOrderedList;
+    case EditAction::eInsertUnorderedListElement:
+    case EditAction::eRemoveUnorderedListElement:
+      return EditorInputType::eInsertUnorderedList;
+    case EditAction::eInsertHorizontalRuleElement:
+      return EditorInputType::eInsertHorizontalRule;
+    case EditAction::eDrop:
+      return EditorInputType::eInsertFromDrop;
+    case EditAction::ePaste:
+      return EditorInputType::eInsertFromPaste;
+    case EditAction::eUpdateComposition:
+      return EditorInputType::eInsertCompositionText;
+    case EditAction::eCommitComposition:
+      if (StaticPrefs::dom_input_events_conform_to_level_1()) {
+        return EditorInputType::eInsertCompositionText;
+      }
+      return EditorInputType::eInsertFromComposition;
+    case EditAction::eCancelComposition:
+      if (StaticPrefs::dom_input_events_conform_to_level_1()) {
+        return EditorInputType::eInsertCompositionText;
+      }
+      return EditorInputType::eDeleteCompositionText;
+    case EditAction::eDeleteByComposition:
+      if (StaticPrefs::dom_input_events_conform_to_level_1()) {
+        // XXX Or EditorInputType::eDeleteContent?  I don't know which IME may
+        //     causes this situation.
+        return EditorInputType::eInsertCompositionText;
+      }
+      return EditorInputType::eDeleteByComposition;
+    case EditAction::eInsertLinkElement:
+      return EditorInputType::eInsertLink;
+    case EditAction::eDeleteWordBackward:
+      return EditorInputType::eDeleteWordBackward;
+    case EditAction::eDeleteWordForward:
+      return EditorInputType::eDeleteWordForward;
+    case EditAction::eDeleteToBeginningOfSoftLine:
+      return EditorInputType::eDeleteSoftLineBackward;
+    case EditAction::eDeleteToEndOfSoftLine:
+      return EditorInputType::eDeleteSoftLineForward;
+    case EditAction::eDeleteByDrag:
+      return EditorInputType::eDeleteByDrag;
+    case EditAction::eCut:
+      return EditorInputType::eDeleteByCut;
+    case EditAction::eDeleteSelection:
+    case EditAction::eRemoveTableRowElement:
+    case EditAction::eRemoveTableColumn:
+    case EditAction::eRemoveTableElement:
+    case EditAction::eDeleteTableCellContents:
+    case EditAction::eRemoveTableCellElement:
+      return EditorInputType::eDeleteContent;
+    case EditAction::eDeleteBackward:
+      return EditorInputType::eDeleteContentBackward;
+    case EditAction::eDeleteForward:
+      return EditorInputType::eDeleteContentForward;
+    case EditAction::eUndo:
+      return EditorInputType::eHistoryUndo;
+    case EditAction::eRedo:
+      return EditorInputType::eHistoryRedo;
+    case EditAction::eSetFontWeightProperty:
+    case EditAction::eRemoveFontWeightProperty:
+      return EditorInputType::eFormatBold;
+    case EditAction::eSetTextStyleProperty:
+    case EditAction::eRemoveTextStyleProperty:
+      return EditorInputType::eFormatItalic;
+    case EditAction::eSetTextDecorationPropertyUnderline:
+    case EditAction::eRemoveTextDecorationPropertyUnderline:
+      return EditorInputType::eFormatUnderline;
+    case EditAction::eSetTextDecorationPropertyLineThrough:
+    case EditAction::eRemoveTextDecorationPropertyLineThrough:
+      return EditorInputType::eFormatStrikeThrough;
+    case EditAction::eSetVerticalAlignPropertySuper:
+    case EditAction::eRemoveVerticalAlignPropertySuper:
+      return EditorInputType::eFormatSuperscript;
+    case EditAction::eSetVerticalAlignPropertySub:
+    case EditAction::eRemoveVerticalAlignPropertySub:
+      return EditorInputType::eFormatSubscript;
+    case EditAction::eJustify:
+      return EditorInputType::eFormatJustifyFull;
+    case EditAction::eAlignCenter:
+      return EditorInputType::eFormatJustifyCenter;
+    case EditAction::eAlignRight:
+      return EditorInputType::eFormatJustifyRight;
+    case EditAction::eAlignLeft:
+      return EditorInputType::eFormatJustifyLeft;
+    case EditAction::eIndent:
+      return EditorInputType::eFormatIndent;
+    case EditAction::eOutdent:
+      return EditorInputType::eFormatOutdent;
+    case EditAction::eRemoveAllInlineStyleProperties:
+      return EditorInputType::eFormatRemove;
+    case EditAction::eSetTextDirection:
+      return EditorInputType::eFormatSetBlockTextDirection;
+    case EditAction::eSetBackgroundColorPropertyInline:
+    case EditAction::eRemoveBackgroundColorPropertyInline:
+      return EditorInputType::eFormatBackColor;
+    case EditAction::eSetColorProperty:
+    case EditAction::eRemoveColorProperty:
+      return EditorInputType::eFormatFontColor;
+    case EditAction::eSetFontFamilyProperty:
+    case EditAction::eRemoveFontFamilyProperty:
+      return EditorInputType::eFormatFontName;
+    default:
+      return EditorInputType::eUnknown;
+  }
+}
+
 }  // namespace mozilla
 
 inline bool operator!(const mozilla::EditSubAction& aEditSubAction) {
   return aEditSubAction == mozilla::EditSubAction::eNone;
 }
 
 #endif  // #ifdef mozilla_EditAction_h
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -2012,24 +2012,34 @@ void EditorBase::NotifyEditorObservers(
       }
       break;
     default:
       MOZ_CRASH("Handle all notifications here");
       break;
   }
 }
 
-void EditorBase::FireInputEvent() {
+void EditorBase::FireInputEvent(EditAction aEditAction) {
+  MOZ_ASSERT(IsEditActionDataAvailable());
+
+  // We don't need to dispatch multiple input events if there is a pending
+  // input event.  However, it may have different event target.  If we resolved
+  // this issue, we need to manage the pending events in an array.  But it's
+  // overwork.  We don't need to do it for the very rare case.
+  // TODO: However, we start to set InputEvent.inputType.  So, each "input"
+  //       event now notifies web app each change.  So, perhaps, we should
+  //       not omit input events.
+
   RefPtr<Element> targetElement = GetInputEventTargetElement();
   if (NS_WARN_IF(!targetElement)) {
     return;
   }
   RefPtr<TextEditor> textEditor = AsTextEditor();
-  DebugOnly<nsresult> rvIgnored =
-      nsContentUtils::DispatchInputEvent(targetElement, textEditor);
+  DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(
+      targetElement, ToInputType(aEditAction), textEditor);
   NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                        "Failed to dispatch input event");
 }
 
 NS_IMETHODIMP
 EditorBase::AddEditActionListener(nsIEditActionListener* aListener) {
   NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
 
--- a/editor/libeditor/EditorBase.h
+++ b/editor/libeditor/EditorBase.h
@@ -1766,18 +1766,24 @@ class EditorBase : public nsIEditor,
    * SelectAllInternal() should be used instead of SelectAll() in editor
    * because SelectAll() creates AutoEditActionSetter but we should avoid
    * to create it as far as possible.
    */
   virtual nsresult SelectAllInternal();
 
   nsresult DetermineCurrentDirection();
 
+  /**
+   * FireInputEvent() dispatches an "input" event synchronously or
+   * asynchronously if it's not safe to dispatch.
+   */
   MOZ_CAN_RUN_SCRIPT
-  void FireInputEvent();
+  void FireInputEvent() { FireInputEvent(GetEditAction()); }
+  MOZ_CAN_RUN_SCRIPT
+  void FireInputEvent(EditAction aEditAction);
 
   /**
    * Called after a transaction is done successfully.
    */
   void DoAfterDoTransaction(nsITransaction* aTxn);
 
   /**
    * Called after a transaction is undone successfully.
--- a/editor/libeditor/TextEditorDataTransfer.cpp
+++ b/editor/libeditor/TextEditorDataTransfer.cpp
@@ -301,17 +301,17 @@ nsresult TextEditor::OnDrop(DragEvent* a
     }
     droppedAt = SelectionRefPtr()->FocusRef();
     if (NS_WARN_IF(!droppedAt.IsSet())) {
       return NS_ERROR_FAILURE;
     }
 
     // Let's fire "input" event for the deletion now.
     if (mDispatchInputEvent) {
-      FireInputEvent();
+      FireInputEvent(EditAction::eDeleteByDrag);
       if (NS_WARN_IF(Destroyed())) {
         return NS_ERROR_EDITOR_DESTROYED;
       }
     }
 
     // XXX Now, Selection may be changed by input event listeners.  If so,
     //     should we update |droppedAt|?
   }
--- a/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
+++ b/editor/libeditor/tests/test_abs_positioner_positioning_elements.html
@@ -79,16 +79,18 @@ SimpleTest.waitForFocus(async function()
           return;
         }
         ok(aEvent instanceof InputEvent,
            '"input" event should be dispatched with InputEvent interface');
         is(aEvent.cancelable, false,
            '"input" event should be never cancelable');
         is(aEvent.bubbles, true,
            '"input" event should always bubble');
+        is(aEvent.inputType, "",
+           "inputType should be empty string when an element is moved");
       }
 
       content.addEventListener("input", onInput);
 
       // Click on the positioner.
       synthesizeMouse(target, kPositionerX, kPositionerY, {type: "mousedown"});
       // Drag it delta pixels.
       synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mousemove"});
--- a/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
+++ b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html
@@ -8,38 +8,40 @@
   <link rel="stylesheet" type="text/css"
           href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <div id="display">
   <iframe id="editor1" srcdoc="<html><body contenteditable id='eventTarget'></body></html>"></iframe>
   <iframe id="editor2" srcdoc="<html contenteditable id='eventTarget'><body></body></html>"></iframe>
   <iframe id="editor3" srcdoc="<html><body><div contenteditable id='eventTarget'></div></body></html>"></iframe>
-  <iframe id="editor4" srcdoc="<html contenteditable id='eventTarget'><body><div contenteditable id='editTarget'></div></body></html>"></iframe>
+  <iframe id="editor4" srcdoc="<html contenteditable id='eventTarget'><body><div contenteditable></div></body></html>"></iframe>
   <iframe id="editor5" srcdoc="<html><body id='eventTarget'></body><script>document.designMode='on';</script></html>"></iframe>
 </div>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(runTests, window);
 
+const kIsWin = navigator.platform.indexOf("Win") == 0;
 const kIsMac = navigator.platform.indexOf("Mac") == 0;
 
 function runTests() {
   function doTests(aDocument, aWindow, aDescription) {
     aDescription += ": ";
     aWindow.focus();
 
     var body = aDocument.body;
+    var selection = aWindow.getSelection();
 
     var eventTarget = aDocument.getElementById("eventTarget");
     // The event target must be focusable because it's the editing host.
     eventTarget.focus();
 
     var editTarget = aDocument.getElementById("editTarget");
     if (!editTarget) {
       editTarget = eventTarget;
@@ -87,77 +89,257 @@ function runTests() {
 
     aWindow.addEventListener("input", handler, true);
 
     inputEvent = null;
     synthesizeKey("a", { }, aWindow);
     is(editTarget.innerHTML, "a", aDescription + "wrong element was edited");
     ok(inputEvent, aDescription + "input event wasn't fired by 'a' key");
     ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event");
+    is(inputEvent.inputType, "insertText",
+       aDescription + 'inputType should be "insertText" when typing "a"');
 
     inputEvent = null;
-    synthesizeKey("VK_BACK_SPACE", { }, aWindow);
+    synthesizeKey("KEY_Backspace", { }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key");
     ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event");
+    is(inputEvent.inputType, "deleteContentBackward",
+       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with collapsed selection');
 
     inputEvent = null;
     synthesizeKey("B", { shiftKey: true }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by 'B' key");
     ok(inputEvent.isTrusted, aDescription + "input event by 'B' key wasn't trusted event");
+    is(inputEvent.inputType, "insertText",
+       aDescription + 'inputType should be "insertText" when typing "B"');
 
     inputEvent = null;
-    synthesizeKey("VK_RETURN", { }, aWindow);
+    synthesizeKey("KEY_Enter", { }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by Enter key");
     ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event");
+    is(inputEvent.inputType, "insertParagraph",
+       aDescription + 'inputType should be "insertParagraph" when pressing "Enter"');
 
     inputEvent = null;
     synthesizeKey("C", { shiftKey: true }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by 'C' key");
     ok(inputEvent.isTrusted, aDescription + "input event by 'C' key wasn't trusted event");
+    is(inputEvent.inputType, "insertText",
+       aDescription + 'inputType should be "insertText" when typing "C"');
 
     inputEvent = null;
-    synthesizeKey("VK_RETURN", { }, aWindow);
+    synthesizeKey("KEY_Enter", { }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by Enter key (again)");
     ok(inputEvent.isTrusted, aDescription + "input event by Enter key (again) wasn't trusted event");
+    is(inputEvent.inputType, "insertParagraph",
+       aDescription + 'inputType should be "insertParagraph" when pressing "Enter" again');
 
     inputEvent = null;
     editTarget.innerHTML = "foo-bar";
     ok(!inputEvent, aDescription + "input event was fired by setting value");
 
     inputEvent = null;
     editTarget.innerHTML = "";
     ok(!inputEvent, aDescription + "input event was fired by setting empty value");
 
     inputEvent = null;
     synthesizeKey(" ", { }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by Space key");
     ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event");
+    is(inputEvent.inputType, "insertText",
+       aDescription + 'inputType should be "insertText" when typing " "');
 
     inputEvent = null;
-    synthesizeKey("VK_DELETE", { }, aWindow);
+    synthesizeKey("KEY_Delete", { }, aWindow);
     ok(!inputEvent, aDescription + "input event was fired by Delete key at the end");
 
     inputEvent = null;
-    synthesizeKey("VK_LEFT", { }, aWindow);
+    synthesizeKey("KEY_ArrowLeft", { }, aWindow);
     ok(!inputEvent, aDescription + "input event was fired by Left key");
 
     inputEvent = null;
-    synthesizeKey("VK_DELETE", { }, aWindow);
+    synthesizeKey("KEY_Delete", { }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start");
     ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event");
+    is(inputEvent.inputType, "deleteContentForward",
+       aDescription + 'inputType should be "deleteContentForward" when pressing "Delete" with collapsed selection');
 
     inputEvent = null;
     synthesizeKey("z", { accelKey: true }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by Undo");
     ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event");
+    is(inputEvent.inputType, "historyUndo",
+       aDescription + 'inputType should be "historyUndo" when doing "Undo"');
 
     inputEvent = null;
     synthesizeKey("z", { accelKey: true, shiftKey: true }, aWindow);
     ok(inputEvent, aDescription + "input event wasn't fired by Redo");
     ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event");
+    is(inputEvent.inputType, "historyRedo",
+       aDescription + 'inputType should be "historyRedo" when doing "Redo"');
+
+    inputEvent = null;
+    synthesizeKey("KEY_Enter", {shiftKey: true}, aWindow);
+    ok(inputEvent, aDescription + "input event wasn't fired by Shift + Enter key");
+    ok(inputEvent.isTrusted, aDescription + "input event by Shift + Enter key wasn't trusted event");
+    is(inputEvent.inputType, "insertLineBreak",
+       aDescription + 'inputType should be "insertLineBreak" when pressing Shift + "Enter"');
+
+    // Backspace/Delete with non-collapsed selection.
+    editTarget.innerHTML = "a";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    inputEvent = null;
+    synthesizeKey("KEY_Backspace", {}, aWindow);
+    ok(inputEvent,
+       aDescription + 'input event should be fired by pressing "Backspace" with non-collapsed selection');
+    ok(inputEvent.isTrusted,
+       aDescription + 'input event should be trusted when pressing "Backspace" with non-collapsed selection');
+    is(inputEvent.inputType, "deleteContentBackward",
+       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with non-collapsed selection');
+
+    editTarget.innerHTML = "a";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    inputEvent = null;
+    synthesizeKey("KEY_Delete", {}, aWindow);
+    ok(inputEvent,
+       aDescription + 'input event should be fired by pressing "Delete" with non-collapsed selection');
+    ok(inputEvent.isTrusted,
+       aDescription + 'input event should be trusted when pressing "Delete" with non-collapsed selection');
+    is(inputEvent.inputType, "deleteContentForward",
+       aDescription + 'inputType should be "deleteContentBackward" when Delete "Backspace" with non-collapsed selection');
+
+    // Delete to previous/next word boundary with collapsed selection.
+    editTarget.innerHTML = "a";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    selection.collapseToEnd();
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to previous word boundary with collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to previous word boundary with collapsed selection");
+    is(inputEvent.inputType, "deleteWordBackward",
+       aDescription + 'inputType should be "deleteWordBackward" when deleting to previous word boundary with collapsed selection');
+
+    editTarget.innerHTML = "a";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    selection.collapseToStart();
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to next word boundary with collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to next word boundary with collapsed selection");
+    is(inputEvent.inputType, "deleteWordForward",
+       aDescription + 'inputType should be "deleteWordForward" when deleting to next word boundary with collapsed selection');
+
+    // Delete to previous/next word boundary with non-collapsed selection.
+    editTarget.innerHTML = "abc";
+    editTarget.focus();
+    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to previous word boundary with non-collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to previous word boundary with non-collapsed selection");
+    if (kIsWin) {
+      // Only on Windows, we collapse selection to start before handling this command.
+      is(inputEvent.inputType, "deleteWordBackward",
+         aDescription + 'inputType should be "deleteWordBackward" when deleting to previous word boundary with non-collapsed selection');
+    } else {
+      is(inputEvent.inputType, "deleteContentBackward",
+         aDescription + 'inputType should be "deleteContentBackward" when deleting to previous word boundary with non-collapsed selection');
+    }
+
+    editTarget.innerHTML = "abc";
+    editTarget.focus();
+    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to next word boundary with non-collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to next word boundary with non-collapsed selection");
+    if (kIsWin) {
+      // Only on Windows, we collapse selection to start before handling this command.
+      is(inputEvent.inputType, "deleteWordForward",
+         aDescription + 'inputType should be "deleteWordForward" when deleting to next word boundary with non-collapsed selection');
+    } else {
+      is(inputEvent.inputType, "deleteContentForward",
+         aDescription + 'inputType should be "deleteContentForward" when deleting to next word boundary with non-collapsed selection');
+    }
+
+    // Delete to previous/next visual line boundary with collapsed selection.
+    editTarget.innerHTML = "a";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    selection.collapseToEnd();
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to previous visual line boundary with collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to previous visual line boundary with collapsed selection");
+    is(inputEvent.inputType, "deleteSoftLineBackward",
+       aDescription + 'inputType should be "deleteSoftLineBackward" when deleting to previous visual line boundary with collapsed selection');
+
+    editTarget.innerHTML = "a";
+    editTarget.focus();
+    selection.selectAllChildren(editTarget);
+    selection.collapseToStart();
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to next visual line boundary with collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to next visual line boundary with collapsed selection");
+    is(inputEvent.inputType, "deleteSoftLineForward",
+       aDescription + 'inputType should be "deleteSoftLineForward" when deleting to visual line boundary with collapsed selection');
+
+    // Delete to previous/next visual line boundary with non-collapsed selection.
+    editTarget.innerHTML = "abc";
+    editTarget.focus();
+    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to previous visual line boundary with non-collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to previous visual line boundary with non-collapsed selection");
+    if (kIsWin) {
+      // Only on Windows, we collapse selection to start before handling this command.
+      is(inputEvent.inputType, "deleteSoftLineBackward",
+         aDescription + 'inputType should be "deleteSoftLineBackward" when deleting to next visual line boundary with non-collapsed selection');
+    } else {
+      is(inputEvent.inputType, "deleteContentBackward",
+         aDescription + 'inputType should be "deleteContentBackward" when deleting to previous visual line boundary with non-collapsed selection');
+    }
+
+    editTarget.innerHTML = "abc";
+    editTarget.focus();
+    selection.setBaseAndExtent(editTarget.firstChild, 1, editTarget.firstChild, 2);
+    inputEvent = null;
+    SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine");
+    ok(inputEvent,
+       aDescription + "input event should be fired by deleting to next visual line boundary with non-collapsed selection");
+    ok(inputEvent.isTrusted,
+       aDescription + "input event should be trusted when deleting to next visual line boundary with non-collapsed selection");
+    if (kIsWin) {
+      // Only on Windows, we collapse selection to start before handling this command.
+      is(inputEvent.inputType, "deleteSoftLineForward",
+         aDescription + 'inputType should be "deleteSoftLineForward" when deleting to next visual line boundary with non-collapsed selection');
+    } else {
+      is(inputEvent.inputType, "deleteContentForward",
+         aDescription + 'inputType should be "deleteContentForward" when deleting to next visual line boundary with non-collapsed selection');
+    }
 
     aWindow.removeEventListener("input", handler, true);
   }
 
   doTests(document.getElementById("editor1").contentDocument,
           document.getElementById("editor1").contentWindow,
           "Editor1, body has contenteditable attribute");
   doTests(document.getElementById("editor2").contentDocument,
--- a/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
+++ b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html
@@ -61,29 +61,35 @@ function runTests() {
 
     aElement.addEventListener("input", handler, true);
 
     inputEvent = null;
     sendString("a");
     is(aElement.value, "a", aDescription + "'a' key didn't change the value");
     ok(inputEvent, aDescription + "input event wasn't fired by 'a' key");
     ok(inputEvent.isTrusted, aDescription + "input event by 'a' key wasn't trusted event");
+    is(inputEvent.inputType, "insertText",
+       aDescription + 'inputType should be "insertText" when typing "a"');
 
     inputEvent = null;
     synthesizeKey("KEY_Backspace");
     is(aElement.value, "", aDescription + "BackSpace key didn't remove the value");
     ok(inputEvent, aDescription + "input event wasn't fired by BackSpace key");
     ok(inputEvent.isTrusted, aDescription + "input event by BackSpace key wasn't trusted event");
+    is(inputEvent.inputType, "deleteContentBackward",
+       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with collapsed selection');
 
     if (aIsTextarea) {
       inputEvent = null;
       synthesizeKey("KEY_Enter");
       is(aElement.value, "\n", aDescription + "Enter key didn't change the value");
       ok(inputEvent, aDescription + "input event wasn't fired by Enter key");
       ok(inputEvent.isTrusted, aDescription + "input event by Enter key wasn't trusted event");
+      is(inputEvent.inputType, "insertLineBreak",
+         aDescription + 'inputType should be "insertLineBreak" when pressing "Enter"');
     }
 
     inputEvent = null;
     aElement.value = "foo-bar";
     is(aElement.value, "foo-bar", aDescription + "value wasn't set");
     ok(!inputEvent, aDescription + "input event was fired by setting value");
 
     inputEvent = null;
@@ -91,44 +97,75 @@ function runTests() {
     is(aElement.value, "", aDescription + "value wasn't set (empty)");
     ok(!inputEvent, aDescription + "input event was fired by setting empty value");
 
     inputEvent = null;
     sendString(" ");
     is(aElement.value, " ", aDescription + "Space key didn't change the value");
     ok(inputEvent, aDescription + "input event wasn't fired by Space key");
     ok(inputEvent.isTrusted, aDescription + "input event by Space key wasn't trusted event");
+    is(inputEvent.inputType, "insertText",
+       aDescription + 'inputType should be "insertText" when typing " "');
 
     inputEvent = null;
     synthesizeKey("KEY_Delete");
     is(aElement.value, " ", aDescription + "Delete key removed the value");
     ok(!inputEvent, aDescription + "input event was fired by Delete key at the end");
 
     inputEvent = null;
     synthesizeKey("KEY_ArrowLeft");
     is(aElement.value, " ", aDescription + "Left key removed the value");
     ok(!inputEvent, aDescription + "input event was fired by Left key");
 
     inputEvent = null;
     synthesizeKey("KEY_Delete");
     is(aElement.value, "", aDescription + "Delete key didn't remove the value");
     ok(inputEvent, aDescription + "input event wasn't fired by Delete key at the start");
     ok(inputEvent.isTrusted, aDescription + "input event by Delete key wasn't trusted event");
+    is(inputEvent.inputType, "deleteContentForward",
+       aDescription + 'inputType should be "deleteContentForward" when pressing "Delete" with collapsed selection');
 
     inputEvent = null;
     synthesizeKey("z", {accelKey: true});
     is(aElement.value, " ", aDescription + "Accel+Z key didn't undo the value");
     ok(inputEvent, aDescription + "input event wasn't fired by Undo");
     ok(inputEvent.isTrusted, aDescription + "input event by Undo wasn't trusted event");
+    is(inputEvent.inputType, "historyUndo",
+       aDescription + 'inputType should be "historyUndo" when doing "Undo"');
 
     inputEvent = null;
     synthesizeKey("Z", {accelKey: true, shiftKey: true});
     is(aElement.value, "", aDescription + "Accel+Y key didn't redo the value");
     ok(inputEvent, aDescription + "input event wasn't fired by Redo");
     ok(inputEvent.isTrusted, aDescription + "input event by Redo wasn't trusted event");
+    is(inputEvent.inputType, "historyRedo",
+       aDescription + 'inputType should be "historyRedo" when doing "Redo"');
+
+    // Backspace/Delete with non-collapsed selection.
+    aElement.value = "a";
+    aElement.select();
+    inputEvent = null;
+    synthesizeKey("KEY_Backspace");
+    ok(inputEvent,
+       aDescription + 'input event should be fired by pressing "Backspace" with non-collapsed selection');
+    ok(inputEvent.isTrusted,
+       aDescription + 'input event should be trusted when pressing "Backspace" with non-collapsed selection');
+    is(inputEvent.inputType, "deleteContentBackward",
+       aDescription + 'inputType should be "deleteContentBackward" when pressing "Backspace" with non-collapsed selection');
+
+    aElement.value = "a";
+    aElement.select();
+    inputEvent = null;
+    synthesizeKey("KEY_Delete");
+    ok(inputEvent,
+       aDescription + 'input event should be fired by pressing "Delete" with non-collapsed selection');
+    ok(inputEvent.isTrusted,
+       aDescription + 'input event should be trusted when pressing "Delete" with non-collapsed selection');
+    is(inputEvent.inputType, "deleteContentForward",
+       aDescription + 'inputType should be "deleteContentBackward" when Delete "Backspace" with non-collapsed selection');
 
     aElement.removeEventListener("input", handler, true);
   }
 
   doTests(document.getElementById("input"), "<input type=\"text\">", false);
   doTests(document.getElementById("textarea"), "<textarea>", true);
 
   SimpleTest.finish();
--- a/editor/libeditor/tests/test_dragdrop.html
+++ b/editor/libeditor/tests/test_dragdrop.html
@@ -24,16 +24,17 @@ SimpleTest.waitForExplicitFinish();
 var shouldClear = false;
 window.addEventListener("dragstart", function(event) { if (shouldClear) event.dataTransfer.clearData(); }, true);
 
 function checkInputEvent(aEvent, aExpectedTarget, aDescription) {
   ok(aEvent instanceof InputEvent, `"input" event should be dispatched with InputEvent interface ${aDescription}`);
   is(aEvent.cancelable, false, `"input" event should be never cancelable ${aDescription}`);
   is(aEvent.bubbles, true, `"input" event should always bubble ${aDescription}`);
   is(aEvent.target, aExpectedTarget, `"input" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element ${aDescription}`);
+  is(aEvent.inputType, "insertFromDrop", `inputType should be "insertFromDrop" on the <${aExpectedTarget.tagName.toLowerCase()}> element ${aDescription}`);
 }
 
 function doTest() {
   const htmlContextData = { type: "text/_moz_htmlcontext",
                             data: "<html><body></body></html>" };
   const htmlInfoData = { type: "text/_moz_htmlinfo", data: "0,0" };
   const htmlData = { type: "text/html", data: '<span id="text" style="font-size: 40px;">Some Text</span>' };
 
--- a/editor/libeditor/tests/test_middle_click_paste.html
+++ b/editor/libeditor/tests/test_middle_click_paste.html
@@ -78,16 +78,18 @@ async function copyHTMLContent(aInnerHTM
 
 function checkInputEvent(aEvent, aDescription) {
   ok(aEvent instanceof InputEvent,
      `"input" event should be dispatched with InputEvent interface ${aDescription}`);
   is(aEvent.cancelable, false,
      `"input" event should be never cancelable ${aDescription}`);
   is(aEvent.bubbles, true,
      `"input" event should always bubble ${aDescription}`);
+  is(aEvent.inputType, "insertFromPaste",
+     `inputType should be "insertFromPaste" ${aDescription}`);
 }
 
 async function doTextareaTests(aTextarea) {
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   aTextarea.addEventListener("input", onInput);
--- a/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html
+++ b/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html
@@ -46,16 +46,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
+  is(inputEvents[0].inputType, "insertText",
+     'inputType should be "insertText" after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor');
 
   // Tests when the editor is in HTML editor mode.
   getEditor().flags &= ~SpecialPowers.Ci.nsIPlaintextEditor.eEditorPlaintextMask;
 
   editor.innerHTML = "";
 
   inputEvents = [];
   getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>", "this is cited text", false);
@@ -71,16 +73,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)');
+  is(inputEvents[0].inputType, "",
+     "inputType should be empty string after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext)");
 
   editor.innerHTML = "";
 
   inputEvents = [];
   getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>And here is second line.", "this is cited text", true);
 
   ok(selection.isCollapsed,
      "Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)");
@@ -93,16 +97,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      'One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)');
+  is(inputEvents[0].inputType, "",
+     "inputType should be empty string after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source)");
 
   SimpleTest.finish();
 });
 
 function getEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window);
 }
--- a/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html
+++ b/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html
@@ -20,23 +20,24 @@ SimpleTest.waitForFocus(function() {
   let selection = window.getSelection();
   let description, condition;
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   editor.addEventListener("input", onInput);
 
-  function checkInputEvent(aEvent, aDescription) {
+  function checkInputEvent(aEvent, aInputType, aDescription) {
     if (aEvent.type != "input") {
       return;
     }
     ok(aEvent instanceof InputEvent, `${aDescription}"input" event should be dispatched with InputEvent interface`);
     is(aEvent.cancelable, false, `${aDescription}"input" event should be never cancelable`);
     is(aEvent.bubbles, true, `${aDescription}"input" event should always bubble`);
+    is(aEvent.inputType, aInputType, `${aDescription}inputType should be ${aInputType}`);
   }
 
   function selectFromTextSiblings(aNode) {
     condition = "selecting the node from end of previous text to start of next text node";
     selection.setBaseAndExtent(aNode.previousSibling, aNode.previousSibling.length,
                                aNode.nextSibling, 0);
   }
   function selectNode(aNode) {
@@ -65,178 +66,178 @@ SimpleTest.waitForFocus(function() {
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("b", "");
     is(editor.innerHTML, "<p>test: here is bolden text</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove the <b> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatBold", description);
     }
   }
 
   description = "When there is a <b> element which has style attribute and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = '<p>test: <b style="font-style: italic">here</b> is bolden text</p>';
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("b", "");
     is(editor.innerHTML, '<p>test: <span style="font-style: italic">here</span> is bolden text</p>',
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should replace the <b> element with <span> element to keep the style');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatBold", description);
     }
   }
 
   description = "When there is a <b> element which has class attribute and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = '<p>test: <b class="foo">here</b> is bolden text</p>';
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("b", "");
     is(editor.innerHTML, '<p>test: <span class="foo">here</span> is bolden text</p>',
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should replace the <b> element with <span> element to keep the class');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatBold", description);
     }
   }
 
   description = "When there is a <b> element which has an <i> element and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("b", "");
     is(editor.innerHTML, "<p>test: <i>here</i> is bolden and italic text</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove only the <b> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatBold", description);
     }
   }
 
   description = "When there is a <b> element which has an <i> element and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("i", "");
     is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatItalic", description);
     }
   }
 
   description = "When there is an <i> element in a <b> element and ";
   for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling.firstChild);
     getHTMLEditor().removeInlineProperty("b", "");
     is(editor.innerHTML, "<p>test: <i>here</i> is bolden and italic text</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove only the <b> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatBold", description);
     }
   }
 
   description = "When there is an <i> element in a <b> element and ";
   for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>";
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling.firstChild);
     getHTMLEditor().removeInlineProperty("i", "");
     is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatItalic", description);
     }
   }
 
   description = "When there is an <i> element between text nodes in a <b> element and ";
   for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = "<p>test: <b>h<i>e</i>re</b> is bolden and italic text</p>";
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("i", "");
     is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatItalic", description);
     }
   }
 
   description = "When there is an <i> element between text nodes in a <b> element and ";
   for (let prepare of [selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = "<p>test: <b>h<i>e</i>re</b> is bolden and italic text</p>";
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("b", "");
     is(editor.innerHTML, "<p>test: <b>h</b><i>e</i><b>re</b> is bolden and italic text</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should split the <b> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "formatBold", description);
     }
   }
 
   description = "When there is an <a> element whose href attribute is not empty and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = '<p>test: <a href="about:blank">here</a> is a link</p>';
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("href", "");
     is(editor.innerHTML, "<p>test: here is a link</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should remove the <a> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "", description);
     }
   }
 
   // XXX In the case of "name", removeInlineProperty() does not the <a> element when name attribute is empty.
   description = "When there is an <a> element whose href attribute is empty and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = '<p>test: <a href="">here</a> is a link</p>';
     editor.focus();
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("href", "");
     is(editor.innerHTML, "<p>test: here is a link</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should remove the <a> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "", description);
     }
   }
 
   description = "When there is an <a> element which does not have href attribute and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = "<p>test: <a>here</a> is an anchor</p>";
     editor.focus();
     inputEvents = [];
@@ -268,17 +269,17 @@ SimpleTest.waitForFocus(function() {
     inputEvents = [];
     prepare(editor.firstChild.firstChild.nextSibling);
     getHTMLEditor().removeInlineProperty("name", "");
     is(editor.innerHTML, "<p>test: here is a named anchor</p>",
       description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should remove the <a> element');
     is(inputEvents.length, 1,
       description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause an "input" event');
     if (inputEvents.length > 0) {
-      checkInputEvent(inputEvents[0], description);
+      checkInputEvent(inputEvents[0], "", description);
     }
   }
 
   // XXX In the case of "href", removeInlineProperty() removes the <a> element when href attribute is empty.
   description = "When there is an <a> element whose name attribute is empty and ";
   for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) {
     editor.innerHTML = '<p>test: <a name="">here</a> is a named anchor</p>';
     editor.focus();
--- a/editor/libeditor/tests/test_nsIPlaintextEditor_insertLineBreak.html
+++ b/editor/libeditor/tests/test_nsIPlaintextEditor_insertLineBreak.html
@@ -48,16 +48,18 @@ SimpleTest.waitForFocus(function() {
   is(textarea.value, "abc\ndef", "nsIPlaintextEditor.insertLineBreak() should insert \n into multi-line editor");
   is(inputEvents.length, 1, "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on multi-line editor");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (on multi-line editor)');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (on multi-line editor)');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (on multi-line editor)');
+  is(inputEvents[0].inputType, "insertLineBreak",
+     'inputType should be "insertLineBreak" on multi-line editor');
 
   // Note that despite of the name, insertLineBreak() should insert paragraph separator in HTMLEditor.
 
   document.execCommand("defaultParagraphSeparator", false, "br");
 
   contenteditable.innerHTML = "abcdef";
   contenteditable.focus();
   contenteditable.scrollTop;
@@ -71,16 +73,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"br\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #1');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #1');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "br") #1');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "br") #1');
 
   contenteditable.innerHTML = "<p>abcdef</p>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -90,16 +94,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"br\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #2');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #2');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "br") #2');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "br") #2');
 
   contenteditable.innerHTML = "<div>abcdef</div>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -109,16 +115,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"br\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #3');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #3');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "br") #3');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "br") #3');
 
   contenteditable.innerHTML = "<pre>abcdef</pre>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -128,16 +136,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"br\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "br") #4');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "br") #4');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "br") #4');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "br") #4');
 
   document.execCommand("defaultParagraphSeparator", false, "p");
 
   contenteditable.innerHTML = "abcdef";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild, 3);
   inputEvents = [];
@@ -149,16 +159,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"p\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #1');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #1');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "p") #1');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "p") #1');
 
   contenteditable.innerHTML = "<p>abcdef</p>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -168,16 +180,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"p\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #2');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #2');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "p") #2');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "p") #2');
 
   contenteditable.innerHTML = "<div>abcdef</div>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -187,16 +201,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"p\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #3');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #3');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "p") #3');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "p") #3');
 
   contenteditable.innerHTML = "<pre>abcdef</pre>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -206,16 +222,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"p\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "p") #4');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "p") #4');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "p") #4');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "p") #4');
 
   document.execCommand("defaultParagraphSeparator", false, "div");
 
   contenteditable.innerHTML = "abcdef";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild, 3);
   inputEvents = [];
@@ -227,16 +245,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has only text node when defaultParagraphSeparator is \"div\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #1');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #1');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "div") #1');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "div") #1');
 
   contenteditable.innerHTML = "<p>abcdef</p>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -246,16 +266,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <p> element when defaultParagraphSeparator is \"div\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #2');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #2');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "div") #2');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "div") #2');
 
   contenteditable.innerHTML = "<div>abcdef</div>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -265,16 +287,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <div> element when defaultParagraphSeparator is \"div\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #3');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #3');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "div") #3');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "div") #3');
 
   contenteditable.innerHTML = "<pre>abcdef</pre>";
   contenteditable.focus();
   contenteditable.scrollTop;
   selection.collapse(contenteditable.firstChild.firstChild, 3);
   inputEvents = [];
   contenteditable.addEventListener("input", onInput);
   getPlaintextEditor(contenteditable).insertLineBreak();
@@ -284,16 +308,18 @@ SimpleTest.waitForFocus(function() {
   is(inputEvents.length, 1,
      "nsIPlaintextEditor.insertLineBreak() should cause 'input' event once on contenteditable which has <pre> element when defaultParagraphSeparator is \"div\"");
   ok(inputEvents[0] instanceof InputEvent,
      '"input" event should be dispatched with InputEvent interface (when defaultParagraphSeparator is "div") #4');
   is(inputEvents[0].cancelable, false,
      '"input" event should be never cancelable even if "click" event (when defaultParagraphSeparator is "div") #4');
   is(inputEvents[0].bubbles, true,
      '"input" event should always bubble (when defaultParagraphSeparator is "div") #4');
+  is(inputEvents[0].inputType, "insertParagraph",
+     'inputType should be "insertParagraph" on HTMLEditor (when defaultParagraphSeparator is "div") #4');
 
   SimpleTest.finish();
 });
 
 function getPlaintextEditor(aEditorElement) {
   let editor = aEditorElement ? SpecialPowers.wrap(aEditorElement).editor : null;
   if (!editor) {
     editor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window);
--- a/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html
@@ -21,16 +21,18 @@ SimpleTest.waitForFocus(function() {
 
   function checkInputEvent(aEvent, aDescription) {
     ok(aEvent instanceof InputEvent,
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, "deleteContent",
+       `inputType should be "deleteContent" ${aDescription}`);
   }
 
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   editor.addEventListener("input", onInput);
 
--- a/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html
@@ -21,16 +21,18 @@ SimpleTest.waitForFocus(function() {
 
   function checkInputEvent(aEvent, aDescription) {
     ok(aEvent instanceof InputEvent,
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, "deleteContent",
+       `inputType should be "deleteContent" ${aDescription}`);
   }
 
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   editor.addEventListener("input", onInput);
 
--- a/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html
@@ -14,267 +14,374 @@
 
 <script class="testbody" type="application/javascript">
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editor = document.getElementById("content");
   let selection = document.getSelection();
 
+  function checkInputEvent(aEvent, aDescription) {
+    ok(aEvent instanceof InputEvent,
+       `"input" event should be dispatched with InputEvent interface ${aDescription}`);
+    is(aEvent.cancelable, false,
+       `"input" event should be never cancelable ${aDescription}`);
+    is(aEvent.bubbles, true,
+       `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, "deleteContent",
+       `inputType should be "deleteContent" ${aDescription}`);
+  }
+
+  let inputEvents = [];
+  function onInput(aEvent) {
+    inputEvents.push(aEvent);
+  }
+  editor.addEventListener("input", onInput);
+
+  inputEvents = [];
   selection.collapse(editor.firstChild, 0);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(1) should do nothing if selection is not in <table>");
+  is(inputEvents.length, 0,
+     'No "input" event should be fired when a call of nsITableEditor.deleteTableColumn(1) does nothing');
 
   selection.removeAllRanges();
   try {
+    inputEvents = [];
     getTableEditor().deleteTableColumn(1);
     ok(false, "getTableEditor().deleteTableColumn(1) without selection ranges should throw exception");
   } catch (e) {
     ok(true, "getTableEditor().deleteTableColumn(1) without selection ranges should throw exception");
+    is(inputEvents.length, 0,
+       'No "input" event should be fired when nsITableEditor.deleteTableColumn(1) causes exception due to no selection range');
   }
 
   // If a cell is selected and the argument is less than number of rows,
   // specified number of rows should be removed starting from the row
   // containing the selected cell.  But if the argument is same or
   // larger than actual number of rows, the <table> should be removed.
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   let range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(1) should delete the first column when a cell in the first column is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the first column is selected');
+  checkInputEvent(inputEvents[0], "when a cell in the first column is selected");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(1) should delete the second column when a cell in the second column is selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the second column is selected');
+  checkInputEvent(inputEvents[0], "when a cell in the second column is selected");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(2) should delete the <table> since there is only 2 columns");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in first column is selected and argument is same as number of rows');
+  checkInputEvent(inputEvents[0], "when a cell in first column is selected and argument is same as number of rows");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(3);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(3) should delete the <table> when argument is larger than actual number of columns");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when argument is larger than actual number of columns');
+  checkInputEvent(inputEvents[0], "when argument is larger than actual number of columns");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(2);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(2) should delete the second column containing selected cell and next column");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in second column and argument is same as the remaining columns');
+  checkInputEvent(inputEvents[0], "when a cell in second column and argument is same as the remaining columns");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(3);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(3) should delete the <table> since the argument equals actual number of columns");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in first column and argument is larger than the remaining columns');
+  checkInputEvent(inputEvents[0], "when a cell in first column and argument is larger than the remaining columns");
 
   // Similar to selected a cell, when selection is in a cell, the cell should
   // treated as selected.
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(1) should delete the first column when a cell in the first column contains selection range");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the first column contains selection range');
+  checkInputEvent(inputEvents[0], "when a cell in the first column contains selection range");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td></tr></table>';
+  inputEvents = [];
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(1) should delete the second column when a cell in the second column contains selection range");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in the second column contains selection range');
+  checkInputEvent(inputEvents[0], "when a cell in the second column contains selection range");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableColumn(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(2) should delete the <table> since there is only 2 columns");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell in first column is selected and argument includes next row');
+  checkInputEvent(inputEvents[0], "when all text in a cell in first column is selected and argument includes next row");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableColumn(3);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(3) should delete the <table> when argument is larger than actual number of columns");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell in first column is selected and argument is same as number of all rows');
+  checkInputEvent(inputEvents[0], "when all text in a cell in first column is selected and argument is same as number of all rows");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableColumn(2);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(2) should delete the second column containing a cell containing selection range and next column");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell is selected and argument is same than renaming number of columns');
+  checkInputEvent(inputEvents[0], "when all text in a cell is selected and argument is same than renaming number of columns");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>';
+  inputEvents = [];
   editor.scrollTop; // Needs to flush pending reflow since we need layout information in this case.
   range = document.createRange();
   range.selectNode(document.getElementById("select").firstChild);
   selection.addRange(range);
   getTableEditor().deleteTableColumn(3);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(3) should delete the <table> since the argument equals actual number of columns");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when all text in a cell in the first column and argument is larger than renaming number of columns');
+  checkInputEvent(inputEvents[0], "when all text in a cell in the first column and argument is larger than renaming number of columns");
 
   // The argument should be ignored when 2 or more cells are selected.
   // XXX Different from deleteTableRow(), this removes the <table> completely.
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select2">cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(1) should delete the <table> when both columns have selected cell");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when both columns have selected cell');
+  checkInputEvent(inputEvents[0], "when both columns have selected cell");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(2) should delete the <table> since 2 is number of columns of the <table>");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when cells in every column are selected #2');
+  checkInputEvent(inputEvents[0], "when cells in every column are selected #2");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(2);
   is(editor.innerHTML, "",
      "nsITableEditor.deleteTableColumn(2) should delete the <table> since 2 is number of columns of the <table>");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 2 cells in same column are selected');
+  checkInputEvent(inputEvents[0], "when 2 cells in same column are selected");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-3</td></tr><tr><td>cell2-3</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(1) should delete first 2 columns because cells in the both columns are selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 2 cell elements in different columns are selected #1');
+  checkInputEvent(inputEvents[0], "when 2 cell elements in different columns are selected #1");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td><td id="select2">cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select1"));
   selection.addRange(range);
   range = document.createRange();
   range.selectNode(document.getElementById("select2"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>",
      "nsITableEditor.deleteTableColumn(1) should delete the first and the last columns because cells in the both columns are selected");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when 2 cell elements in different columns are selected #2');
+  checkInputEvent(inputEvents[0], "when 2 cell elements in different columns are selected #2");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select" colspan="2">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, '<table><tbody><tr><td id="select" colspan="1"><br></td><td>cell1-3</td></tr><tr><td>cell2-2</td><td>cell2-3</td></tr></tbody></table>',
      "nsITableEditor.deleteTableColumn(1) with a selected cell is colspan=\"2\" should delete the first column and add empty cell to the second column");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell is selected and its colspan is 2');
+  checkInputEvent(inputEvents[0], "when a cell is selected and its colspan is 2");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td id="select" colspan="3">cell1-1</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, '<table><tbody><tr><td id="select" colspan="2"><br></td></tr><tr><td>cell2-2</td><td>cell2-3</td></tr></tbody></table>',
      "nsITableEditor.deleteTableColumn(1) with a selected cell is colspan=\"3\" should delete the first column and add empty cell whose colspan is 2 to the second column");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell is selected and its colspan is 3');
+  checkInputEvent(inputEvents[0], "when a cell is selected and its colspan is 3");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td colspan="3">cell1-1</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, '<table><tbody><tr><td colspan="2">cell1-1</td></tr><tr><td>cell2-1</td><td>cell2-3</td></tr></tbody></table>',
      "nsITableEditor.deleteTableColumn(1) with selected cell in the second column should delete the second column and the colspan in the first row should be adjusted");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in 2nd column is only cell defined by the column #1');
+  checkInputEvent(inputEvents[0], "when a cell in 2nd column is only cell defined by the column #1");
 
   selection.removeAllRanges();
   editor.innerHTML =
     '<table><tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr></table>';
+  inputEvents = [];
   range = document.createRange();
   range.selectNode(document.getElementById("select"));
   selection.addRange(range);
   getTableEditor().deleteTableColumn(1);
   is(editor.innerHTML, '<table><tbody><tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-3</td></tr></tbody></table>',
      "nsITableEditor.deleteTableColumn(1) with selected cell in the second column should delete the second column and the colspan should be adjusted");
+  is(inputEvents.length, 1,
+     'Only one "input" event should be fired when a cell in 2nd column is only cell defined by the column #2');
+  checkInputEvent(inputEvents[0], "when a cell in 2nd column is only cell defined by the column #2");
 
   SimpleTest.finish();
 });
 
 function getTableEditor() {
   var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
   return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
 }
--- a/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html
+++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html
@@ -21,16 +21,18 @@ SimpleTest.waitForFocus(function() {
 
   function checkInputEvent(aEvent, aDescription) {
     ok(aEvent instanceof InputEvent,
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, "deleteContent",
+       `inputType should be "deleteContent" ${aDescription}`);
   }
 
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   editor.addEventListener("input", onInput);
 
--- a/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html
@@ -21,16 +21,18 @@ SimpleTest.waitForFocus(function() {
 
   function checkInputEvent(aEvent, aDescription) {
     ok(aEvent instanceof InputEvent,
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, "",
+       `inputType should be empty string ${aDescription}`);
   }
 
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   editor.addEventListener("input", onInput);
 
--- a/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html
@@ -21,16 +21,18 @@ SimpleTest.waitForFocus(function() {
 
   function checkInputEvent(aEvent, aDescription) {
     ok(aEvent instanceof InputEvent,
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, "",
+       `inputType should be empty string ${aDescription}`);
   }
 
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   editor.addEventListener("input", onInput);
 
--- a/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html
+++ b/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html
@@ -21,16 +21,18 @@ SimpleTest.waitForFocus(function() {
 
   function checkInputEvent(aEvent, aDescription) {
     ok(aEvent instanceof InputEvent,
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, "",
+       `inputType should be empty string ${aDescription}`);
   }
 
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
   editor.addEventListener("input", onInput);
 
--- a/editor/libeditor/tests/test_resizers_resizing_elements.html
+++ b/editor/libeditor/tests/test_resizers_resizing_elements.html
@@ -92,16 +92,18 @@ SimpleTest.waitForFocus(async function()
           return;
         }
         ok(aEvent instanceof InputEvent,
            '"input" event should be dispatched with InputEvent interface');
         is(aEvent.cancelable, false,
            '"input" event should be never cancelable');
         is(aEvent.bubbles, true,
            '"input" event should always bubble');
+        is(aEvent.inputType, "",
+           `inputType should be empty string when an element is resized`);
       }
 
       content.addEventListener("input", onInput);
 
       // Click on the correct resizer
       synthesizeMouse(target, basePosX, basePosY, {type: "mousedown"});
       // Drag it delta pixels to the right and bottom (or maybe left and top!)
       synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mousemove"});
--- a/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html
+++ b/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html
@@ -16,23 +16,25 @@ SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(() => {
   let textarea = document.getElementById("textarea");
   let editor = SpecialPowers.wrap(textarea).editor;
 
   let inlineSpellChecker = editor.getInlineSpellChecker(true);
 
   textarea.focus();
 
-  function checkInputEvent(aEvent, aDescription) {
+  function checkInputEvent(aEvent, aInputType, aDescription) {
     ok(aEvent instanceof InputEvent,
        `"input" event should be dispatched with InputEvent interface ${aDescription}`);
     is(aEvent.cancelable, false,
        `"input" event should be never cancelable ${aDescription}`);
     is(aEvent.bubbles, true,
        `"input" event should always bubble ${aDescription}`);
+    is(aEvent.inputType, aInputType,
+       `inputType should be "${aInputType}" ${aDescription}`);
   }
 
   let inputEvents = [];
   function onInput(aEvent) {
     inputEvents.push(aEvent);
   }
 
   SpecialPowers.Cu.import(
@@ -47,33 +49,33 @@ SimpleTest.waitForFocus(() => {
       is(misspelledWord.endOffset, 7,
          "Misspelled word should end at 7");
       inputEvents = [];
       inlineSpellChecker.replaceWord(editor.rootElement.firstChild, 5, "aux");
       is(textarea.value, "abc aux abc",
          "'abx' should be replaced with 'aux'");
       is(inputEvents.length, 1,
          'Only one "input" event should be fired when replacing a word with spellchecker');
-      checkInputEvent(inputEvents[0], "when replacing a word with spellchecker");
+      checkInputEvent(inputEvents[0], "insertReplacementText", "when replacing a word with spellchecker");
 
       inputEvents = [];
       synthesizeKey("z", { accelKey: true });
       is(textarea.value, "abc abx abc",
          "'abx' should be restored by undo");
       is(inputEvents.length, 1,
          'Only one "input" event should be fired when undoing the replacing word');
-      checkInputEvent(inputEvents[0], "when undoing the replacing word");
+      checkInputEvent(inputEvents[0], "historyUndo", "when undoing the replacing word");
 
       inputEvents = [];
       synthesizeKey("z", { accelKey: true, shiftKey: true });
       is(textarea.value, "abc aux abc",
          "'aux' should be restored by redo");
       is(inputEvents.length, 1,
          'Only one "input" event should be fired when redoing the replacing word');
-      checkInputEvent(inputEvents[0], "when redoing the replacing word");
+      checkInputEvent(inputEvents[0], "historyRedo", "when redoing the replacing word");
 
       textarea.removeEventListener("input", onInput);
 
       SimpleTest.finish();
     });
   });
 });
 </script>
--- a/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html
+++ b/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html
@@ -24,55 +24,57 @@ https://bugzilla.mozilla.org/show_bug.cg
 <script class="testbody" type="application/javascript">
 SimpleTest.waitForExplicitFinish();
 SimpleTest.waitForFocus(function() {
   let editableElements = [
     document.getElementById("input"),
     document.getElementById("textarea"),
   ];
   for (let editableElement of editableElements) {
-    function checkInputEvent(aEvent, aDescription) {
+    function checkInputEvent(aEvent, aInputType, aDescription) {
       ok(aEvent instanceof InputEvent,
          `"input" event should be dispatched with InputEvent interface ${aDescription}`);
       is(aEvent.cancelable, false,
          `"input" event should be never cancelable ${aDescription}`);
       is(aEvent.bubbles, true,
          `"input" event should always bubble ${aDescription}`);
+      is(aEvent.inputType, aInputType,
+         `inputType should be "${aInputType}" ${aDescription}`);
     }
 
     let inputEvents = [];
     function onInput(aEvent) {
       inputEvents.push(aEvent);
     }
     editableElement.addEventListener("input", onInput);
 
     editableElement.focus();
 
     inputEvents = [];
     synthesizeKey("a");
     is(inputEvents.length, 1,
        `Only one "input" event should be fired when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
-    checkInputEvent(inputEvents[0], `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], "insertText", `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`);
 
     inputEvents = [];
     synthesizeKey("c");
     is(inputEvents.length, 1,
        `Only one "input" event should be fired when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
-    checkInputEvent(inputEvents[0], `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], "insertText", `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`);
 
     inputEvents = [];
     synthesizeKey("KEY_ArrowLeft");
     is(inputEvents.length, 0,
        `No "input" event should be fired when pressing "ArrowLeft" key on <${editableElement.tagName.toLowerCase()}> element`);
 
     inputEvents = [];
     synthesizeKey("b");
     is(inputEvents.length, 1,
        `Only one "input" event should be fired when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
-    checkInputEvent(inputEvents[0], `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], "insertText", `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`);
 
     let editor = SpecialPowers.wrap(editableElement).editor;
     let transactionManager = editor.transactionManager;
     is(transactionManager.numberOfUndoItems, 2,
        editableElement.tagName + ": Initially, there should be 2 undo items");
     // Defined as nsITextControlElement::DEFAULT_UNDO_CAP
     is(transactionManager.maxTransactionCount, 1000,
        editableElement.tagName + ": Initially, transaction manager should be able to have 1,000 undo items");
@@ -86,25 +88,25 @@ SimpleTest.waitForFocus(function() {
        editableElement.tagName + ": After setting value, all undo items must be deleted");
     is(transactionManager.maxTransactionCount, 1000,
        editableElement.tagName + ": After setting value, maximum transaction count should be restored to the previous value");
 
     inputEvents = [];
     synthesizeKey("a");
     is(inputEvents.length, 1,
        `Only one "input" event should be fired when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
-    checkInputEvent(inputEvents[0], `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], "insertText", `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`);
 
     inputEvents = [];
     synthesizeKey("z", { accelKey: true });
     is(editableElement.value, "def",
        editableElement.tagName + ": undo should work after setting value");
     is(inputEvents.length, 1,
        `Only one "input" event should be fired when undoing on <${editableElement.tagName.toLowerCase()}> element`);
-    checkInputEvent(inputEvents[0], `when undoing on <${editableElement.tagName.toLowerCase()}> element`);
+    checkInputEvent(inputEvents[0], "historyUndo", `when undoing on <${editableElement.tagName.toLowerCase()}> element`);
 
     // Disable undo/redo.
     editor.enableUndo(0);
     is(transactionManager.maxTransactionCount, 0,
        editableElement.tagName + ": Transaction manager should not be able to have undo items");
     editableElement.value = "hij";
     is(transactionManager.maxTransactionCount, 0,
        editableElement.tagName + ": Transaction manager should not be able to have undo items after setting value");
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -110,16 +110,18 @@ namespace layers {
 
 using namespace mozilla::ipc;
 using namespace mozilla::gfx;
 using namespace std;
 
 using base::ProcessId;
 using base::Thread;
 
+using mozilla::Telemetry::LABELS_CONTENT_FRAME_TIME_REASON;
+
 /// Equivalent to asserting CompositorThreadHolder::IsInCompositorThread with
 /// the addition that it doesn't assert if the compositor thread holder is
 /// already gone during late shutdown.
 static void AssertIsInCompositorThread() {
   MOZ_RELEASE_ASSERT(!CompositorThread() ||
                      CompositorThreadHolder::IsInCompositorThread());
 }
 
@@ -2407,10 +2409,135 @@ mozilla::ipc::IPCResult CompositorBridge
   return IPC_OK();
 #else
   MOZ_ASSERT_UNREACHABLE(
       "CompositorBridgeParent::RecvAllPluginsCaptured calls unexpected.");
   return IPC_FAIL_NO_REASON(this);
 #endif
 }
 
+int32_t RecordContentFrameTime(
+    const VsyncId& aTxnId, const TimeStamp& aVsyncStart,
+    const TimeStamp& aTxnStart, const VsyncId& aCompositeId,
+    const TimeStamp& aCompositeEnd, const TimeDuration& aFullPaintTime,
+    const TimeDuration& aVsyncRate, bool aContainsSVGGroup,
+    bool aRecordUploadStats, wr::RendererStats* aStats /* = nullptr */) {
+  double latencyMs = (aCompositeEnd - aTxnStart).ToMilliseconds();
+  double latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+  int32_t fracLatencyNorm = lround(latencyNorm * 100.0);
+
+#ifdef MOZ_GECKO_PROFILER
+  if (profiler_is_active()) {
+    class ContentFramePayload : public ProfilerMarkerPayload {
+     public:
+      ContentFramePayload(const mozilla::TimeStamp& aStartTime,
+                          const mozilla::TimeStamp& aEndTime)
+          : ProfilerMarkerPayload(aStartTime, aEndTime) {}
+      virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                                 const TimeStamp& aProcessStartTime,
+                                 UniqueStacks& aUniqueStacks) override {
+        StreamCommonProps("CONTENT_FRAME_TIME", aWriter, aProcessStartTime,
+                          aUniqueStacks);
+      }
+    };
+    profiler_add_marker_for_thread(
+        profiler_current_thread_id(), "CONTENT_FRAME_TIME",
+        MakeUnique<ContentFramePayload>(aTxnStart, aCompositeEnd));
+  }
+#endif
+
+  Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME, fracLatencyNorm);
+
+  if (!(aTxnId == VsyncId()) && aVsyncStart) {
+    latencyMs = (aCompositeEnd - aVsyncStart).ToMilliseconds();
+    latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+    fracLatencyNorm = lround(latencyNorm * 100.0);
+    int32_t result = fracLatencyNorm;
+    Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_VSYNC, fracLatencyNorm);
+
+    if (aContainsSVGGroup) {
+      Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITH_SVG,
+                            fracLatencyNorm);
+    }
+
+    // Record CONTENT_FRAME_TIME_REASON.
+    //
+    // Note that deseralizing a layers update (RecvUpdate) can delay the receipt
+    // of the composite vsync message
+    // (CompositorBridgeParent::CompositeToTarget), since they're using the same
+    // thread. This can mean that compositing might start significantly late,
+    // but this code will still detect it as having successfully started on the
+    // right vsync (which is somewhat correct). We'd now have reduced time left
+    // in the vsync interval to finish compositing, so the chances of a missed
+    // frame increases. This is effectively including the RecvUpdate work as
+    // part of the 'compositing' phase for this metric, but it isn't included in
+    // COMPOSITE_TIME, and *is* included in CONTENT_FULL_PAINT_TIME.
+    //
+    // Also of note is that when the root WebRenderBridgeParent decides to
+    // skip a composite (due to the Renderer being busy), that won't notify
+    // child WebRenderBridgeParents. That failure will show up as the
+    // composite starting late (since it did), but it's really a fault of a
+    // slow composite on the previous frame, not a slow
+    // CONTENT_FULL_PAINT_TIME. It would be nice to have a separate bucket for
+    // this category (scene was ready on the next vsync, but we chose not to
+    // composite), but I can't find a way to locate the right child
+    // WebRenderBridgeParents from the root. WebRender notifies us of the
+    // child pipelines contained within a render, after it finishes, but I
+    // can't see how to query what child pipeline would have been rendered,
+    // when we choose to not do it.
+    if (fracLatencyNorm < 200) {
+      // Success
+      Telemetry::AccumulateCategorical(
+          LABELS_CONTENT_FRAME_TIME_REASON::OnTime);
+    } else {
+      if (aCompositeId == VsyncId() || aTxnId >= aCompositeId) {
+        // Vsync ids are nonsensical, possibly something got trigged from
+        // outside vsync?
+        Telemetry::AccumulateCategorical(
+            LABELS_CONTENT_FRAME_TIME_REASON::NoVsync);
+      } else if (aCompositeId - aTxnId > 1) {
+        // Composite started late (and maybe took too long as well)
+        if (aFullPaintTime >= TimeDuration::FromMilliseconds(20)) {
+          Telemetry::AccumulateCategorical(
+              LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeLong);
+        } else if (aFullPaintTime >= TimeDuration::FromMilliseconds(10)) {
+          Telemetry::AccumulateCategorical(
+              LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeMid);
+        } else if (aFullPaintTime >= TimeDuration::FromMilliseconds(5)) {
+          Telemetry::AccumulateCategorical(
+              LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeLow);
+        } else {
+          Telemetry::AccumulateCategorical(
+              LABELS_CONTENT_FRAME_TIME_REASON::MissedComposite);
+        }
+      } else {
+        // Composite started on time, but must have taken too long.
+        Telemetry::AccumulateCategorical(
+            LABELS_CONTENT_FRAME_TIME_REASON::SlowComposite);
+      }
+    }
+
+    if (aRecordUploadStats) {
+      if (aStats) {
+        latencyMs -= (double(aStats->resource_upload_time) / 1000000.0);
+        latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+        fracLatencyNorm = lround(latencyNorm * 100.0);
+      }
+      Telemetry::Accumulate(
+          Telemetry::CONTENT_FRAME_TIME_WITHOUT_RESOURCE_UPLOAD,
+          fracLatencyNorm);
+
+      if (aStats) {
+        latencyMs -= (double(aStats->gpu_cache_upload_time) / 1000000.0);
+        latencyNorm = latencyMs / aVsyncRate.ToMilliseconds();
+        fracLatencyNorm = lround(latencyNorm * 100.0);
+      }
+      Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITHOUT_UPLOAD,
+                            fracLatencyNorm);
+    }
+    return result;
+  }
+
+  return 0;
+}
+
 }  // namespace layers
 }  // namespace mozilla
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -699,12 +699,19 @@ class CompositorBridgeParent final : pub
   // indicates if the plugin windows were hidden, and need to be made
   // visible again even if their geometry has not changed.
   bool mPluginWindowsHidden;
 #endif
 
   DISALLOW_EVIL_CONSTRUCTORS(CompositorBridgeParent);
 };
 
+int32_t RecordContentFrameTime(
+    const VsyncId& aTxnId, const TimeStamp& aVsyncStart,
+    const TimeStamp& aTxnStart, const VsyncId& aCompositeId,
+    const TimeStamp& aCompositeEnd, const TimeDuration& aFullPaintTime,
+    const TimeDuration& aVsyncRate, bool aContainsSVGGroup,
+    bool aRecordUploadStats, wr::RendererStats* aStats = nullptr);
+
 }  // namespace layers
 }  // namespace mozilla
 
 #endif  // mozilla_layers_CompositorBridgeParent_h
--- a/gfx/layers/ipc/CrossProcessCompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CrossProcessCompositorBridgeParent.cpp
@@ -386,17 +386,17 @@ void CrossProcessCompositorBridgeParent:
 #endif
   Telemetry::Accumulate(
       Telemetry::CONTENT_FULL_PAINT_TIME,
       static_cast<uint32_t>(
           (endTime - aInfo.transactionStart()).ToMilliseconds()));
 
   aLayerTree->SetPendingTransactionId(
       aInfo.id(), aInfo.vsyncId(), aInfo.vsyncStart(), aInfo.refreshStart(),
-      aInfo.transactionStart(), aInfo.url(), aInfo.fwdTime());
+      aInfo.transactionStart(), endTime, aInfo.url(), aInfo.fwdTime());
 }
 
 void CrossProcessCompositorBridgeParent::DidCompositeLocked(
     LayersId aId, const VsyncId& aVsyncId, TimeStamp& aCompositeStart,
     TimeStamp& aCompositeEnd) {
   sIndirectLayerTreesLock->AssertCurrentThreadOwns();
   if (LayerTransactionParent* layerTree = sIndirectLayerTrees[aId].mLayerTree) {
     TransactionId transactionId =
--- a/gfx/layers/ipc/LayerTransactionParent.cpp
+++ b/gfx/layers/ipc/LayerTransactionParent.cpp
@@ -882,62 +882,19 @@ void LayerTransactionParent::DeallocShme
 
 bool LayerTransactionParent::IsSameProcess() const {
   return OtherPid() == base::GetCurrentProcId();
 }
 
 TransactionId LayerTransactionParent::FlushTransactionId(
     const VsyncId& aId, TimeStamp& aCompositeEnd) {
   if (mId.IsValid() && mPendingTransaction.IsValid() && !mVsyncRate.IsZero()) {
-    double latencyMs = (aCompositeEnd - mTxnStartTime).ToMilliseconds();
-    double latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
-    int32_t fracLatencyNorm = lround(latencyNorm * 100.0);
-    Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME, fracLatencyNorm);
-
-    if (!(mTxnVsyncId == VsyncId()) && mVsyncStartTime) {
-      latencyMs = (aCompositeEnd - mVsyncStartTime).ToMilliseconds();
-      latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
-      fracLatencyNorm = lround(latencyNorm * 100.0);
-      Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_VSYNC,
-                            fracLatencyNorm);
-
-      // Record CONTENT_FRAME_TIME_REASON. See
-      // WebRenderBridgeParent::FlushTransactionIdsForEpoch for more details.
-      //
-      // Note that deseralizing a layers update (RecvUpdate) can delay the receipt
-      // of the composite vsync message
-      // (CompositorBridgeParent::CompositeToTarget), since they're using the same
-      // thread. This can mean that compositing might start significantly late,
-      // but this code will still detect it as having successfully started on the
-      // right vsync (which is somewhat correct). We'd now have reduced time left
-      // in the vsync interval to finish compositing, so the chances of a missed
-      // frame increases. This is effectively including the RecvUpdate work as
-      // part of the 'compositing' phase for this metric, but it isn't included in
-      // COMPOSITE_TIME, and *is* included in CONTENT_FULL_PAINT_TIME.
-      if (fracLatencyNorm < 200) {
-        // Success
-        Telemetry::AccumulateCategorical(
-            LABELS_CONTENT_FRAME_TIME_REASON::OnTime);
-      } else {
-        if (mTxnVsyncId == VsyncId() || aId == VsyncId() || mTxnVsyncId >= aId) {
-          // Vsync ids are nonsensical, possibly something got trigged from
-          // outside vsync?
-          Telemetry::AccumulateCategorical(
-              LABELS_CONTENT_FRAME_TIME_REASON::NoVsync);
-        } else if (aId - mTxnVsyncId > 1) {
-          // Composite started late (and maybe took too long as well)
-          Telemetry::AccumulateCategorical(
-              LABELS_CONTENT_FRAME_TIME_REASON::MissedComposite);
-        } else {
-          // Composite start on time, but must have taken too long.
-          Telemetry::AccumulateCategorical(
-              LABELS_CONTENT_FRAME_TIME_REASON::SlowComposite);
-        }
-      }
-    }
+    RecordContentFrameTime(mTxnVsyncId, mVsyncStartTime, mTxnStartTime, aId,
+                           aCompositeEnd, mTxnEndTime - mTxnStartTime,
+                           mVsyncRate, false, false);
   }
 
 #if defined(ENABLE_FRAME_LATENCY_LOG)
   if (mPendingTransaction.IsValid()) {
     if (mRefreshStartTime) {
       int32_t latencyMs =
           lround((aCompositeEnd - mRefreshStartTime).ToMilliseconds());
       printf_stderr(
--- a/gfx/layers/ipc/LayerTransactionParent.h
+++ b/gfx/layers/ipc/LayerTransactionParent.h
@@ -71,23 +71,25 @@ class LayerTransactionParent final : pub
 
   bool IsSameProcess() const override;
 
   const TransactionId& GetPendingTransactionId() { return mPendingTransaction; }
   void SetPendingTransactionId(TransactionId aId, const VsyncId& aVsyncId,
                                const TimeStamp& aVsyncStartTime,
                                const TimeStamp& aRefreshStartTime,
                                const TimeStamp& aTxnStartTime,
+                               const TimeStamp& aTxnEndTime,
                                const nsCString& aURL,
                                const TimeStamp& aFwdTime) {
     mPendingTransaction = aId;
     mTxnVsyncId = aVsyncId;
     mVsyncStartTime = aVsyncStartTime;
     mRefreshStartTime = aRefreshStartTime;
     mTxnStartTime = aTxnStartTime;
+    mTxnEndTime = aTxnEndTime;
     mTxnURL = aURL;
     mFwdTime = aFwdTime;
   }
   TransactionId FlushTransactionId(const VsyncId& aId,
                                    TimeStamp& aCompositeEnd);
 
   // CompositableParentManager
   void SendAsyncMessage(
@@ -205,16 +207,17 @@ class LayerTransactionParent final : pub
 
   TimeDuration mVsyncRate;
 
   TransactionId mPendingTransaction;
   VsyncId mTxnVsyncId;
   TimeStamp mVsyncStartTime;
   TimeStamp mRefreshStartTime;
   TimeStamp mTxnStartTime;
+  TimeStamp mTxnEndTime;
   TimeStamp mFwdTime;
   nsCString mTxnURL;
 
   // When the widget/frame/browser stuff in this process begins its
   // destruction process, we need to Disconnect() all the currently
   // live shadow layers, because some of them might be orphaned from
   // the layer tree.  This happens in Destroy() above.  After we
   // Destroy() ourself, there's a window in which that information
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -35,18 +35,16 @@
 #include "mozilla/Unused.h"
 #include "mozilla/webrender/RenderThread.h"
 #include "mozilla/widget/CompositorWidget.h"
 
 #ifdef XP_WIN
 #include "dwrite.h"
 #endif
 
-using mozilla::Telemetry::LABELS_CONTENT_FRAME_TIME_REASON;
-
 #ifdef MOZ_GECKO_PROFILER
 #include "ProfilerMarkerPayload.h"
 #endif
 
 bool is_in_main_thread() { return NS_IsMainThread(); }
 
 bool is_in_compositor_thread() {
   return mozilla::layers::CompositorThreadHolder::IsInCompositorThread();
@@ -1911,139 +1909,38 @@ TransactionId WebRenderBridgeParent::Flu
     const auto& transactionId = mPendingTransactionIds.front();
 
     if (aEpoch.mHandle < transactionId.mEpoch.mHandle) {
       break;
     }
 
     if (!IsRootWebRenderBridgeParent() && !mVsyncRate.IsZero() &&
         transactionId.mUseForTelemetry) {
-      double latencyMs =
-          (aEndTime - transactionId.mTxnStartTime).ToMilliseconds();
-      double latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
-      int32_t fracLatencyNorm = lround(latencyNorm * 100.0);
+      auto fullPaintTime =
+          transactionId.mSceneBuiltTime
+              ? transactionId.mSceneBuiltTime - transactionId.mTxnStartTime
+              : TimeDuration::FromMilliseconds(0);
 
-#ifdef MOZ_GECKO_PROFILER
-      if (profiler_is_active()) {
-        class ContentFramePayload : public ProfilerMarkerPayload {
-         public:
-          ContentFramePayload(const mozilla::TimeStamp& aStartTime,
-                              const mozilla::TimeStamp& aEndTime)
-              : ProfilerMarkerPayload(aStartTime, aEndTime) {}
-          virtual void StreamPayload(SpliceableJSONWriter& aWriter,
-                                     const TimeStamp& aProcessStartTime,
-                                     UniqueStacks& aUniqueStacks) override {
-            StreamCommonProps("CONTENT_FRAME_TIME", aWriter, aProcessStartTime,
-                              aUniqueStacks);
-          }
-        };
-        profiler_add_marker_for_thread(
-            profiler_current_thread_id(), "CONTENT_FRAME_TIME",
-            MakeUnique<ContentFramePayload>(transactionId.mTxnStartTime,
-                                            aEndTime));
-      }
-#endif
-
-      if (fracLatencyNorm > 200) {
+      int32_t contentFrameTime = RecordContentFrameTime(
+          transactionId.mVsyncId, transactionId.mVsyncStartTime,
+          transactionId.mTxnStartTime, aCompositeStartId, aEndTime,
+          fullPaintTime, mVsyncRate, transactionId.mContainsSVGGroup, true,
+          aStats);
+      if (contentFrameTime > 200) {
         aOutputStats->AppendElement(FrameStats(
             transactionId.mId, aCompositeStartTime, aRenderStartTime, aEndTime,
-            fracLatencyNorm,
+            contentFrameTime,
             aStats ? (double(aStats->resource_upload_time) / 1000000.0) : 0.0,
             aStats ? (double(aStats->gpu_cache_upload_time) / 1000000.0) : 0.0,
             transactionId.mTxnStartTime, transactionId.mRefreshStartTime,
             transactionId.mFwdTime, transactionId.mSceneBuiltTime,
             transactionId.mSkippedComposites, transactionId.mTxnURL));
-      }
 
-      Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME, fracLatencyNorm);
-      if (fracLatencyNorm > 200) {
         wr::RenderThread::Get()->NotifySlowFrame(mApi->GetId());
       }
-      if (transactionId.mContainsSVGGroup) {
-        Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITH_SVG,
-                              fracLatencyNorm);
-      }
-
-      if (aStats) {
-        latencyMs -= (double(aStats->resource_upload_time) / 1000000.0);
-        latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
-        fracLatencyNorm = lround(latencyNorm * 100.0);
-      }
-      Telemetry::Accumulate(
-          Telemetry::CONTENT_FRAME_TIME_WITHOUT_RESOURCE_UPLOAD,
-          fracLatencyNorm);
-
-      if (aStats) {
-        latencyMs -= (double(aStats->gpu_cache_upload_time) / 1000000.0);
-        latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
-        fracLatencyNorm = lround(latencyNorm * 100.0);
-      }
-      Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_WITHOUT_UPLOAD,
-                            fracLatencyNorm);
-
-      if (!(transactionId.mVsyncId == VsyncId()) &&
-          transactionId.mVsyncStartTime) {
-        latencyMs = (aEndTime - transactionId.mVsyncStartTime).ToMilliseconds();
-        latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
-        fracLatencyNorm = lround(latencyNorm * 100.0);
-        Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME_VSYNC,
-                              fracLatencyNorm);
-
-        // Record CONTENT_FRAME_TIME_REASON.
-        //
-        // Also of note is that when the root WebRenderBridgeParent decides to
-        // skip a composite (due to the Renderer being busy), that won't notify
-        // child WebRenderBridgeParents. That failure will show up as the
-        // composite starting late (since it did), but it's really a fault of a
-        // slow composite on the previous frame, not a slow
-        // CONTENT_FULL_PAINT_TIME. It would be nice to have a separate bucket for
-        // this category (scene was ready on the next vsync, but we chose not to
-        // composite), but I can't find a way to locate the right child
-        // WebRenderBridgeParents from the root. WebRender notifies us of the
-        // child pipelines contained within a render, after it finishes, but I
-        // can't see how to query what child pipeline would have been rendered,
-        // when we choose to not do it.
-        if (fracLatencyNorm < 200) {
-          // Success
-          Telemetry::AccumulateCategorical(
-              LABELS_CONTENT_FRAME_TIME_REASON::OnTime);
-        } else {
-          if (transactionId.mVsyncId == VsyncId() ||
-              aCompositeStartId == VsyncId() ||
-              transactionId.mVsyncId >= aCompositeStartId) {
-            // Vsync ids are nonsensical, possibly something got trigged from
-            // outside vsync?
-            Telemetry::AccumulateCategorical(
-                LABELS_CONTENT_FRAME_TIME_REASON::NoVsync);
-          } else if (aCompositeStartId - transactionId.mVsyncId > 1) {
-            auto fullPaintTime =
-                transactionId.mSceneBuiltTime
-                    ? transactionId.mSceneBuiltTime - transactionId.mTxnStartTime
-                    : TimeDuration::FromMilliseconds(0);
-            // Composite started late (and maybe took too long as well)
-            if (fullPaintTime >= TimeDuration::FromMilliseconds(20)) {
-              Telemetry::AccumulateCategorical(
-                  LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeLong);
-            } else if (fullPaintTime >= TimeDuration::FromMilliseconds(10)) {
-              Telemetry::AccumulateCategorical(
-                  LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeMid);
-            } else if (fullPaintTime >= TimeDuration::FromMilliseconds(5)) {
-              Telemetry::AccumulateCategorical(
-                  LABELS_CONTENT_FRAME_TIME_REASON::MissedCompositeLow);
-            } else {
-              Telemetry::AccumulateCategorical(
-                  LABELS_CONTENT_FRAME_TIME_REASON::MissedComposite);
-            }
-          } else {
-            // Composite start on time, but must have taken too long.
-            Telemetry::AccumulateCategorical(
-                LABELS_CONTENT_FRAME_TIME_REASON::SlowComposite);
-          }
-        }
-      }
     }
 
 #if defined(ENABLE_FRAME_LATENCY_LOG)
     if (transactionId.mRefreshStartTime) {
       int32_t latencyMs =
           lround((aEndTime - transactionId.mRefreshStartTime).ToMilliseconds());
       printf_stderr(
           "From transaction start to end of generate frame latencyMs %d this "
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-b298150b65db9e80ec15aff6877ca3277cb79f92
+1b226534099a24c741e9827c4612eee1ec12d4ee
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -1,30 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{DeviceRect, FilterOp, MixBlendMode, PipelineId, PremultipliedColorF, PictureRect, PicturePoint, WorldPoint};
 use api::{DeviceIntRect, DevicePoint, LayoutRect, PictureToRasterTransform, LayoutPixel, PropertyBinding, PropertyBindingId};
-use api::{DevicePixelScale, RasterRect, RasterSpace, ColorF, ImageKey, DirtyRect, WorldSize, LayoutSize, ClipMode};
-use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor, WorldVector2D};
+use api::{DevicePixelScale, RasterRect, RasterSpace, ColorF, ImageKey, DirtyRect, WorldSize, ClipMode};
+use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor, WorldVector2D, LayoutPoint};
 use box_shadow::{BLUR_SAMPLE_SCALE};
 use clip::{ClipNodeCollector, ClipStore, ClipChainId, ClipChainNode, ClipItem};
 use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex, CoordinateSystemId};
 use device::TextureFilter;
 use euclid::{TypedScale, vec3, TypedRect, TypedPoint2D, TypedSize2D};
 use euclid::approxeq::ApproxEq;
 use intern::ItemUid;
-use internal_types::{FastHashMap, PlaneSplitter};
+use internal_types::{FastHashMap, FastHashSet, PlaneSplitter};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::{TransformPalette, TransformPaletteId, UvRectKind};
 use plane_split::{Clipper, Polygon, Splitter};
 use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, VisibleFace, PrimitiveInstanceKind};
-use prim_store::{get_raster_rects, CoordinateSpaceMapping, VectorKey};
+use prim_store::{get_raster_rects, CoordinateSpaceMapping};
 use prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex};
 use print_tree::PrintTreePrinter;
 use render_backend::FrameResources;
 use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle, TileBlit};
 use render_task::{RenderTaskCacheKey, RenderTaskCacheKeyKind, RenderTaskId, RenderTaskLocation};
 use resource_cache::ResourceCache;
 use scene::{FilterOpHelpers, SceneProperties};
 use scene_builder::DocumentResources;
@@ -123,48 +123,54 @@ pub struct Tile {
     pub handle: TextureCacheHandle,
     /// If true, this tile is marked valid, and the existing texture
     /// cache handle can be used. Tiles are invalidated during the
     /// build_dirty_regions method.
     is_valid: bool,
     /// The tile id is stable between display lists and / or frames,
     /// if the tile is retained. Useful for debugging tile evictions.
     id: TileId,
+    /// The set of transforms that affect primitives on this tile we
+    /// care about. Stored as a set here, and then collected, sorted
+    /// and converted to transform key values during post_update.
+    transforms: FastHashSet<SpatialNodeIndex>,
 }
 
 impl Tile {
     /// Construct a new, invalid tile.
     fn new(
         id: TileId,
     ) -> Self {
         Tile {
             local_rect: LayoutRect::zero(),
             world_rect: WorldRect::zero(),
             valid_rect: WorldRect::zero(),
             visible_rect: None,
             handle: TextureCacheHandle::invalid(),
             descriptor: TileDescriptor::new(),
             is_valid: false,
+            transforms: FastHashSet::default(),
             id,
         }
     }
 
     /// Clear the dependencies for a tile.
     fn clear(&mut self) {
+        self.transforms.clear();
         self.descriptor.clear();
     }
 }
 
 /// Defines a key that uniquely identifies a primitive instance.
 #[derive(Debug, Clone, PartialEq)]
 pub struct PrimitiveDescriptor {
     /// Uniquely identifies the content of the primitive template.
     prim_uid: ItemUid,
-    /// The origin in world space of this primitive.
-    origin: WorldPoint,
+    /// The origin in local space of this primitive.
+    origin: LayoutPoint,
     /// The first clip in the clip_uids array of clips that affect this tile.
     first_clip: u16,
     /// The number of clips that affect this primitive instance.
     clip_count: u16,
 }
 
 /// Uniquely describes the content of this tile, in a way that can be
 /// (reasonably) efficiently hashed and compared.
@@ -174,57 +180,63 @@ pub struct TileDescriptor {
     /// to uniquely describe the content of the primitive template, while
     /// the other parameters describe the clip chain and instance params.
     prims: ComparableVec<PrimitiveDescriptor>,
 
     /// List of clip node unique identifiers. The uid is guaranteed
     /// to uniquely describe the content of the clip node.
     clip_uids: ComparableVec<ItemUid>,
 
-    /// List of tile relative offsets of the clip node origins. This
+    /// List of local offsets of the clip node origins. This
     /// ensures that if a clip node is supplied but has a different
     /// transform between frames that the tile is invalidated.
-    clip_vertices: ComparableVec<VectorKey>,
+    clip_vertices: ComparableVec<LayoutPoint>,
 
     /// List of image keys that this tile depends on.
     image_keys: ComparableVec<ImageKey>,
 
     /// The set of opacity bindings that this tile depends on.
     // TODO(gw): Ugh, get rid of all opacity binding support!
     opacity_bindings: ComparableVec<PropertyBindingId>,
 
     /// List of the required valid rectangles for each primitive.
     needed_rects: Vec<WorldRect>,
 
     /// List of the currently valid rectangles for each primitive.
     current_rects: Vec<WorldRect>,
+
+    /// List of the (quantized) transforms that we care about
+    /// tracking for this tile.
+    transforms: ComparableVec<TransformKey>,
 }
 
 impl TileDescriptor {
     fn new() -> Self {
         TileDescriptor {
             prims: ComparableVec::new(),
             clip_uids: ComparableVec::new(),
             clip_vertices: ComparableVec::new(),
             opacity_bindings: ComparableVec::new(),
             image_keys: ComparableVec::new(),
             needed_rects: Vec::new(),
             current_rects: Vec::new(),
+            transforms: ComparableVec::new(),
         }
     }
 
     /// Clear the dependency information for a tile, when the dependencies
     /// are being rebuilt.
     fn clear(&mut self) {
         self.prims.reset();
         self.clip_uids.reset();
         self.clip_vertices.reset();
         self.opacity_bindings.reset();
         self.image_keys.reset();
         self.needed_rects.clear();
+        self.transforms.reset();
     }
 
     /// Check if the dependencies of this tile are valid.
     fn is_valid(&self) -> bool {
         // For a tile to be valid, it needs to ensure that the currently valid
         // rect of each primitive encloses the required valid rect.
         // TODO(gw): This is only needed for tiles that are partially rendered
         //           (i.e. those clipped to edge of screen). We can make this much
@@ -244,16 +256,17 @@ impl TileDescriptor {
             false
         };
 
         self.image_keys.is_valid() &&
         self.opacity_bindings.is_valid() &&
         self.clip_uids.is_valid() &&
         self.clip_vertices.is_valid() &&
         self.prims.is_valid() &&
+        self.transforms.is_valid() &&
         rects_valid
     }
 }
 
 /// Represents the dirty region of a tile cache picture.
 /// In future, we will want to support multiple dirty
 /// regions.
 #[derive(Debug)]
@@ -684,19 +697,20 @@ impl TileCache {
         }
 
         // Get the tile coordinates in the picture space.
         let (p0, p1) = self.get_tile_coords_for_rect(&world_rect);
 
         // Build the list of resources that this primitive has dependencies on.
         let mut opacity_bindings: SmallVec<[PropertyBindingId; 4]> = SmallVec::new();
         let mut clip_chain_uids: SmallVec<[ItemUid; 8]> = SmallVec::new();
-        let mut clip_vertices: SmallVec<[WorldPoint; 8]> = SmallVec::new();
+        let mut clip_vertices: SmallVec<[LayoutPoint; 8]> = SmallVec::new();
         let mut image_keys: SmallVec<[ImageKey; 8]> = SmallVec::new();
         let mut current_clip_chain_id = prim_instance.clip_chain_id;
+        let mut clip_spatial_nodes = FastHashSet::default();
 
         // Some primitives can not be cached (e.g. external video images)
         let is_cacheable = prim_instance.is_cacheable(
             &resources,
             resource_cache,
         );
 
         // For pictures, we don't (yet) know the valid clip rect, so we can't correctly
@@ -814,29 +828,19 @@ impl TileCache {
                 ClipItem::RoundedRectangle(..) |
                 ClipItem::Image { .. } |
                 ClipItem::BoxShadow(..) => {
                     true
                 }
             };
 
             if add_to_clip_deps {
-                // TODO(gw): Constructing a rect here rather than mapping a point
-                //           is wasteful. We can optimize this by extending the
-                //           SpaceMapper struct to support mapping a point.
-                let local_rect = LayoutRect::new(
-                    clip_chain_node.local_pos,
-                    LayoutSize::zero(),
-                );
-
-                if let Some(clip_world_rect) = self.map_local_to_world.map(&local_rect) {
-                    clip_vertices.push(clip_world_rect.origin);
-                }
-
+                clip_vertices.push(clip_chain_node.local_pos);
                 clip_chain_uids.push(clip_chain_node.handle.uid());
+                clip_spatial_nodes.insert(clip_chain_node.spatial_node_index);
             }
 
             current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
         }
 
         if include_clip_rect {
             self.world_bounding_rect = self.world_bounding_rect.union(&world_clip_rect);
         }
@@ -886,47 +890,29 @@ impl TileCache {
                 tile.is_valid &= is_cacheable;
 
                 // Include any image keys this tile depends on.
                 tile.descriptor.image_keys.extend_from_slice(&image_keys);
 
                 // // Include any opacity bindings this primitive depends on.
                 tile.descriptor.opacity_bindings.extend_from_slice(&opacity_bindings);
 
-                // For the primitive origin, store the world origin relative to
-                // the world origin of the containing picture. This ensures that
-                // a tile with primitives in the same coordinate system as the
-                // container picture itself, but different offsets relative to
-                // the containing picture are correctly invalidated. It does this
-                // while still maintaining the property of keeping the same hash
-                // for different display lists where the local origin is different
-                // but the primitives themselves are at the same relative position.
-                let origin = WorldPoint::new(
-                    world_rect.origin.x - tile.world_rect.origin.x,
-                    world_rect.origin.y - tile.world_rect.origin.y
-                );
-
                 // Update the tile descriptor, used for tile comparison during scene swaps.
                 tile.descriptor.prims.push(PrimitiveDescriptor {
                     prim_uid: prim_instance.uid(),
-                    origin,
+                    origin: prim_instance.prim_origin,
                     first_clip: tile.descriptor.clip_uids.len() as u16,
                     clip_count: clip_chain_uids.len() as u16,
                 });
                 tile.descriptor.clip_uids.extend_from_slice(&clip_chain_uids);
+                tile.descriptor.clip_vertices.extend_from_slice(&clip_vertices);
 
-                // Store tile relative clip vertices.
-                // TODO(gw): We might need to quantize these to avoid
-                //           invalidations due to FP accuracy.
-                for clip_vertex in &clip_vertices {
-                    let clip_vertex = VectorKey {
-                        x: clip_vertex.x - tile.world_rect.origin.x,
-                        y: clip_vertex.y - tile.world_rect.origin.y,
-                    };
-                    tile.descriptor.clip_vertices.push(clip_vertex);
+                tile.transforms.insert(prim_instance.spatial_node_index);
+                for spatial_node_index in &clip_spatial_nodes {
+                    tile.transforms.insert(*spatial_node_index);
                 }
             }
         }
     }
 
     /// Apply any updates after prim dependency updates. This applies
     /// any late tile invalidations, and sets up the dirty rect and
     /// set of tile blits.
@@ -962,16 +948,28 @@ impl TileCache {
         );
 
         let local_clip_rect = map_surface_to_world
             .unmap(&self.world_bounding_rect)
             .expect("bug: unable to map local clip rect");
 
         // Step through each tile and invalidate if the dependencies have changed.
         for (i, tile) in self.tiles.iter_mut().enumerate() {
+            // Update tile transforms
+            let mut transform_spatial_nodes: Vec<SpatialNodeIndex> = tile.transforms.drain().collect();
+            transform_spatial_nodes.sort();
+            for spatial_node_index in transform_spatial_nodes {
+                let mapping: CoordinateSpaceMapping<LayoutPixel, PicturePixel> = CoordinateSpaceMapping::new(
+                    self.spatial_node_index,
+                    spatial_node_index,
+                    frame_context.clip_scroll_tree,
+                ).expect("todo: handle invalid mappings");
+                tile.descriptor.transforms.push(mapping.into());
+            }
+
             // Invalidate if the backing texture was evicted.
             if resource_cache.texture_cache.is_allocated(&tile.handle) {
                 // Request the backing texture so it won't get evicted this frame.
                 // We specifically want to mark the tile texture as used, even
                 // if it's detected not visible below and skipped. This is because
                 // we maintain the set of tiles we care about based on visibility
                 // during pre_update. If a tile still exists after that, we are
                 // assuming that it's either visible or we want to retain it for
@@ -995,17 +993,17 @@ impl TileCache {
                 // If the tile is valid, we will generally want to draw it
                 // on screen. However, if there are no primitives there is
                 // no need to draw it.
                 if !tile.descriptor.prims.is_empty() {
                     self.tiles_to_draw.push(TileIndex(i));
                 }
             } else {
                 // Add the tile rect to the dirty rect.
-                dirty_world_rect = dirty_world_rect.union(&tile.world_rect);
+                dirty_world_rect = dirty_world_rect.union(&visible_rect);
 
                 // Ensure that this texture is allocated.
                 resource_cache.texture_cache.update(
                     &mut tile.handle,
                     descriptor,
                     TextureFilter::Linear,
                     None,
                     [0.0; 3],
--- a/layout/build/nsLayoutModule.cpp
+++ b/layout/build/nsLayoutModule.cpp
@@ -143,16 +143,17 @@ static void Shutdown();
 #include "mozilla/dom/PresentationDeviceManager.h"
 #include "mozilla/dom/PresentationTCPSessionTransport.h"
 
 #include "nsScriptError.h"
 #include "nsBaseCommandController.h"
 #include "nsControllerCommandTable.h"
 
 #include "mozilla/TextInputProcessor.h"
+#include "mozilla/ScriptableContentIterator.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::net;
 using mozilla::dom::power::PowerManagerService;
 using mozilla::dom::quota::QuotaManagerService;
 using mozilla::gmp::GeckoMediaPluginService;
 
@@ -215,16 +216,17 @@ NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR
                                          MediaManager::GetInstance)
 NS_GENERIC_FACTORY_CONSTRUCTOR(PresentationDeviceManager)
 NS_GENERIC_FACTORY_CONSTRUCTOR(TextInputProcessor)
 NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsIPresentationService,
                                          NS_CreatePresentationService)
 NS_GENERIC_FACTORY_CONSTRUCTOR(PresentationTCPSessionTransport)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NotificationTelemetryService, Init)
 NS_GENERIC_FACTORY_CONSTRUCTOR(PushNotifier)
+NS_GENERIC_FACTORY_CONSTRUCTOR(ScriptableContentIterator)
 
 //-----------------------------------------------------------------------------
 
 static bool gInitialized = false;
 
 // Perform our one-time intialization for this module
 
 void nsLayoutModuleInitialize() {
@@ -514,16 +516,18 @@ NS_DEFINE_NAMED_CID(GECKO_MEDIA_PLUGIN_S
 NS_DEFINE_NAMED_CID(PRESENTATION_SERVICE_CID);
 NS_DEFINE_NAMED_CID(PRESENTATION_DEVICE_MANAGER_CID);
 NS_DEFINE_NAMED_CID(PRESENTATION_TCP_SESSION_TRANSPORT_CID);
 
 NS_DEFINE_NAMED_CID(TEXT_INPUT_PROCESSOR_CID);
 
 NS_DEFINE_NAMED_CID(NS_SCRIPTERROR_CID);
 
+NS_DEFINE_NAMED_CID(SCRIPTABLE_CONTENT_ITERATOR_CID);
+
 static nsresult LocalStorageManagerConstructor(nsISupports* aOuter,
                                                REFNSIID aIID, void** aResult) {
   if (NextGenLocalStorageEnabled()) {
     RefPtr<LocalStorageManager2> manager = new LocalStorageManager2();
     return manager->QueryInterface(aIID, aResult);
   }
 
   RefPtr<LocalStorageManager> manager = new LocalStorageManager();
@@ -599,16 +603,17 @@ static const mozilla::Module::CIDEntry k
 #ifdef ACCESSIBILITY
   { &kNS_ACCESSIBILITY_SERVICE_CID, false, nullptr, CreateA11yService },
 #endif
   { &kPRESENTATION_SERVICE_CID, false, nullptr, nsIPresentationServiceConstructor },
   { &kPRESENTATION_DEVICE_MANAGER_CID, false, nullptr, PresentationDeviceManagerConstructor },
   { &kPRESENTATION_TCP_SESSION_TRANSPORT_CID, false, nullptr, PresentationTCPSessionTransportConstructor },
   { &kTEXT_INPUT_PROCESSOR_CID, false, nullptr, TextInputProcessorConstructor },
   { &kNS_SCRIPTERROR_CID, false, nullptr, nsScriptErrorConstructor },
+  { &kSCRIPTABLE_CONTENT_ITERATOR_CID, false, nullptr, ScriptableContentIteratorConstructor },
   { nullptr }
     // clang-format on
 };
 
 static const mozilla::Module::ContractIDEntry kLayoutContracts[] = {
     // clang-format off
   XPCONNECT_CONTRACTS
   { "@mozilla.org/inspector/deep-tree-walker;1", &kIN_DEEPTREEWALKER_CID },
@@ -671,16 +676,17 @@ static const mozilla::Module::ContractID
   { "@mozilla.org/accessibilityService;1", &kNS_ACCESSIBILITY_SERVICE_CID },
 #endif
   { "@mozilla.org/gecko-media-plugin-service;1",  &kGECKO_MEDIA_PLUGIN_SERVICE_CID },
   { PRESENTATION_SERVICE_CONTRACTID, &kPRESENTATION_SERVICE_CID },
   { PRESENTATION_DEVICE_MANAGER_CONTRACTID, &kPRESENTATION_DEVICE_MANAGER_CID },
   { PRESENTATION_TCP_SESSION_TRANSPORT_CONTRACTID, &kPRESENTATION_TCP_SESSION_TRANSPORT_CID },
   { "@mozilla.org/text-input-processor;1", &kTEXT_INPUT_PROCESSOR_CID },
   { NS_SCRIPTERROR_CONTRACTID, &kNS_SCRIPTERROR_CID },
+  { "@mozilla.org/scriptable-content-iterator;1", &kSCRIPTABLE_CONTENT_ITERATOR_CID },
   { nullptr }
 };
 
 static const mozilla::Module::CategoryEntry kLayoutCategories[] = {
   { "content-policy", NS_DATADOCUMENTCONTENTPOLICY_CONTRACTID, NS_DATADOCUMENTCONTENTPOLICY_CONTRACTID },
   { "content-policy", NS_NODATAPROTOCOLCONTENTPOLICY_CONTRACTID, NS_NODATAPROTOCOLCONTENTPOLICY_CONTRACTID },
   { "content-policy", "CSPService", CSPSERVICE_CONTRACTID },
   { "content-policy", NS_MIXEDCONTENTBLOCKER_CONTRACTID, NS_MIXEDCONTENTBLOCKER_CONTRACTID },
--- a/layout/style/ServoCSSPropList.mako.py
+++ b/layout/style/ServoCSSPropList.mako.py
@@ -100,19 +100,19 @@ SERIALIZED_PREDEFINED_TYPES = [
     "FontVariationSettings",
     "FontWeight",
     "Integer",
     "ImageLayer",
     "JustifyContent",
     "JustifyItems",
     "JustifySelf",
     "Length",
-    "LengthOrPercentage",
+    "LengthPercentage",
     "NonNegativeLength",
-    "NonNegativeLengthOrPercentage",
+    "NonNegativeLengthPercentage",
     "ListStyleType",
     "OffsetPath",
     "Opacity",
     "OutlineStyle",
     "OverflowWrap",
     "Position",
     "Quotes",
     "Resize",
--- a/mobile/android/themes/core/aboutMemory.css
+++ b/mobile/android/themes/core/aboutMemory.css
@@ -49,48 +49,52 @@ div.sidebar {
   margin-left: 1em;
 }
 
 div.sidebarContents {
   position: sticky;
   top: 0.5em;
 }
 
-div.index {
+div.sidebarItem {
   padding: 0.5em;
   margin: 1em 0em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
   color: -moz-FieldText;
   -moz-user-select: none;  /* no need to include this when cutting+pasting */
 }
 
-ul.indexList {
+input.filterInput {
+  width: calc(100% - 1em);
+}
+
+ul.index {
   list-style-position: inside;
   margin: 0;
   padding: 0;
 }
 
-ul.indexList > li {
+ul.index > li {
   padding-left: 0.5em;
 }
 
 div.opsRow {
   padding: 0.5em;
   margin-right: 0.5em;
   margin-top: 0.5em;
   border: 1px solid ThreeDShadow;
   border-radius: 10px;
   background: -moz-Field;
   color: -moz-FieldText;
   display: inline-block;
 }
 
-div.opsRowLabel, div.indexLabel {
+div.opsRowLabel, div.sidebarLabel {
   display: block;
   margin-bottom: 0.2em;
   font-weight: bold;
 }
 
 .opsRowLabel label {
   margin-left: 1em;
   font-weight: normal;
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -206,16 +206,25 @@ VARCACHE_PREF(
 // If this is true, "keypress" event's keyCode value and charCode value always
 // become same if the event is not created/initialized by JS.
 VARCACHE_PREF(
   "dom.keyboardevent.keypress.set_keycode_and_charcode_to_same_value",
    dom_keyboardevent_keypress_set_keycode_and_charcode_to_same_value,
   bool, true
 )
 
+// Whether we conform to Input Events Level 1 or Input Events Level 2.
+// true:  conforming to Level 1
+// false: conforming to Level 2
+VARCACHE_PREF(
+  "dom.input_events.conform_to_level_1",
+   dom_input_events_conform_to_level_1,
+  bool, true
+)
+
 // NOTE: This preference is used in unit tests. If it is removed or its default
 // value changes, please update test_sharedMap_var_caches.js accordingly.
 VARCACHE_PREF(
   "dom.webcomponents.shadowdom.report_usage",
    dom_webcomponents_shadowdom_report_usage,
   bool, false
 )
 
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -228,16 +228,19 @@ pref("dom.keyboardevent.keypress.hack.di
 
 // Blacklist of domains of web apps which handle keyCode and charCode of
 // keypress events with a path only for Firefox (i.e., broken if we set
 // non-zero keyCode or charCode value to the other).  The format is exactly
 // same as "dom.keyboardevent.keypress.hack.dispatch_non_printable_keys". So,
 // check its explanation for the detail.
 pref("dom.keyboardevent.keypress.hack.use_legacy_keycode_and_charcode", "");
 
+// Whether InputEvent.inputType is enabled.
+pref("dom.inputevent.inputtype.enabled", true);
+
 // Whether the WebMIDI API is enabled
 pref("dom.webmidi.enabled", false);
 
 // Whether to enable the JavaScript start-up cache. This causes one of the first
 // execution to record the bytecode of the JavaScript function used, and save it
 // in the existing cache entry. On the following loads of the same script, the
 // bytecode would be loaded from the cache instead of being generated once more.
 pref("dom.script_loader.bytecode_cache.enabled", true);
--- a/servo/components/style/attr.rs
+++ b/servo/components/style/attr.rs
@@ -538,17 +538,17 @@ pub fn parse_legacy_color(mut input: &st
             },
         }
     }
 }
 
 /// Parses a [dimension value][dim]. If unparseable, `Auto` is returned.
 ///
 /// [dim]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-dimension-values
-// TODO: this function can be rewritten to return Result<LengthOrPercentage, _>
+// TODO: this function can be rewritten to return Result<LengthPercentage, _>
 pub fn parse_length(mut value: &str) -> LengthOrPercentageOrAuto {
     // Steps 1 & 2 are not relevant
 
     // Step 3
     value = value.trim_start_matches(HTML_SPACE_CHARACTERS);
 
     // Step 4
     if value.is_empty() {
--- a/servo/components/style/gecko/conversions.rs
+++ b/servo/components/style/gecko/conversions.rs
@@ -15,181 +15,136 @@ use crate::gecko_bindings::bindings;
 use crate::gecko_bindings::structs::RawGeckoGfxMatrix4x4;
 use crate::gecko_bindings::structs::{self, nsStyleCoord_CalcValue};
 use crate::gecko_bindings::structs::{nsStyleImage, nsresult, SheetType};
 use crate::gecko_bindings::sugar::ns_style_coord::{CoordData, CoordDataMut, CoordDataValue};
 use crate::stylesheets::{Origin, RulesMutateError};
 use crate::values::computed::image::LineDirection;
 use crate::values::computed::transform::Matrix3D;
 use crate::values::computed::url::ComputedImageUrl;
-use crate::values::computed::{Angle, CalcLengthOrPercentage, Gradient, Image};
-use crate::values::computed::{Integer, LengthOrPercentage};
-use crate::values::computed::{LengthOrPercentageOrAuto, NonNegativeLengthOrPercentageOrAuto};
+use crate::values::computed::{Angle, Gradient, Image};
+use crate::values::computed::{Integer, LengthPercentage};
+use crate::values::computed::{LengthPercentageOrAuto, NonNegativeLengthPercentageOrAuto};
 use crate::values::computed::{Percentage, TextAlign};
 use crate::values::generics::box_::VerticalAlign;
 use crate::values::generics::grid::{TrackListValue, TrackSize};
 use crate::values::generics::image::{CompatMode, GradientItem, Image as GenericImage};
 use crate::values::generics::rect::Rect;
 use crate::values::generics::NonNegative;
 use app_units::Au;
 use std::f32::consts::PI;
+use style_traits::values::specified::AllowedNumericType;
 
-impl From<CalcLengthOrPercentage> for nsStyleCoord_CalcValue {
-    fn from(other: CalcLengthOrPercentage) -> nsStyleCoord_CalcValue {
+impl From<LengthPercentage> for nsStyleCoord_CalcValue {
+    fn from(other: LengthPercentage) -> nsStyleCoord_CalcValue {
         let has_percentage = other.percentage.is_some();
         nsStyleCoord_CalcValue {
             mLength: other.unclamped_length().to_i32_au(),
             mPercent: other.percentage.map_or(0., |p| p.0),
             mHasPercent: has_percentage,
         }
     }
 }
 
-impl From<nsStyleCoord_CalcValue> for CalcLengthOrPercentage {
-    fn from(other: nsStyleCoord_CalcValue) -> CalcLengthOrPercentage {
+impl From<nsStyleCoord_CalcValue> for LengthPercentage {
+    fn from(other: nsStyleCoord_CalcValue) -> LengthPercentage {
         let percentage = if other.mHasPercent {
             Some(Percentage(other.mPercent))
         } else {
             None
         };
-        Self::new(Au(other.mLength).into(), percentage)
+        Self::with_clamping_mode(
+            Au(other.mLength).into(),
+            percentage,
+            AllowedNumericType::All,
+            /* was_calc = */ true,
+        )
     }
 }
 
-impl From<LengthOrPercentage> for nsStyleCoord_CalcValue {
-    fn from(other: LengthOrPercentage) -> nsStyleCoord_CalcValue {
-        match other {
-            LengthOrPercentage::Length(px) => nsStyleCoord_CalcValue {
-                mLength: px.to_i32_au(),
-                mPercent: 0.0,
-                mHasPercent: false,
-            },
-            LengthOrPercentage::Percentage(pc) => nsStyleCoord_CalcValue {
-                mLength: 0,
-                mPercent: pc.0,
-                mHasPercent: true,
-            },
-            LengthOrPercentage::Calc(calc) => calc.into(),
+impl LengthPercentageOrAuto {
+    /// Convert this value in an appropriate `nsStyleCoord::CalcValue`.
+    pub fn to_calc_value(&self) -> Option<nsStyleCoord_CalcValue> {
+        match *self {
+            LengthPercentageOrAuto::LengthPercentage(len) => Some(From::from(len)),
+            LengthPercentageOrAuto::Auto => None,
         }
     }
 }
 
-impl LengthOrPercentageOrAuto {
-    /// Convert this value in an appropriate `nsStyleCoord::CalcValue`.
-    pub fn to_calc_value(&self) -> Option<nsStyleCoord_CalcValue> {
-        match *self {
-            LengthOrPercentageOrAuto::Length(px) => Some(nsStyleCoord_CalcValue {
-                mLength: px.to_i32_au(),
-                mPercent: 0.0,
-                mHasPercent: false,
-            }),
-            LengthOrPercentageOrAuto::Percentage(pc) => Some(nsStyleCoord_CalcValue {
-                mLength: 0,
-                mPercent: pc.0,
-                mHasPercent: true,
-            }),
-            LengthOrPercentageOrAuto::Calc(calc) => Some(calc.into()),
-            LengthOrPercentageOrAuto::Auto => None,
-        }
-    }
-}
-
-impl From<nsStyleCoord_CalcValue> for LengthOrPercentage {
-    fn from(other: nsStyleCoord_CalcValue) -> LengthOrPercentage {
-        match (other.mHasPercent, other.mLength) {
-            (false, _) => LengthOrPercentage::Length(Au(other.mLength).into()),
-            (true, 0) => LengthOrPercentage::Percentage(Percentage(other.mPercent)),
-            _ => LengthOrPercentage::Calc(other.into()),
-        }
-    }
-}
-
-impl From<nsStyleCoord_CalcValue> for LengthOrPercentageOrAuto {
-    fn from(other: nsStyleCoord_CalcValue) -> LengthOrPercentageOrAuto {
-        match (other.mHasPercent, other.mLength) {
-            (false, _) => LengthOrPercentageOrAuto::Length(Au(other.mLength).into()),
-            (true, 0) => LengthOrPercentageOrAuto::Percentage(Percentage(other.mPercent)),
-            _ => LengthOrPercentageOrAuto::Calc(other.into()),
-        }
+impl From<nsStyleCoord_CalcValue> for LengthPercentageOrAuto {
+    fn from(other: nsStyleCoord_CalcValue) -> LengthPercentageOrAuto {
+        LengthPercentageOrAuto::LengthPercentage(LengthPercentage::from(other))
     }
 }
 
 // FIXME(emilio): A lot of these impl From should probably become explicit or
 // disappear as we move more stuff to cbindgen.
-impl From<nsStyleCoord_CalcValue> for NonNegativeLengthOrPercentageOrAuto {
+impl From<nsStyleCoord_CalcValue> for NonNegativeLengthPercentageOrAuto {
     fn from(other: nsStyleCoord_CalcValue) -> Self {
-        use style_traits::values::specified::AllowedNumericType;
-        NonNegative(if other.mLength < 0 || other.mPercent < 0. {
-            LengthOrPercentageOrAuto::Calc(CalcLengthOrPercentage::with_clamping_mode(
+        NonNegative(
+            LengthPercentageOrAuto::LengthPercentage(LengthPercentage::with_clamping_mode(
                 Au(other.mLength).into(),
                 if other.mHasPercent {
                     Some(Percentage(other.mPercent))
                 } else {
                     None
                 },
                 AllowedNumericType::NonNegative,
+                /* was_calc = */ true,
             ))
-        } else {
-            other.into()
-        })
+        )
     }
 }
 
 impl From<Angle> for CoordDataValue {
     fn from(reference: Angle) -> Self {
         CoordDataValue::Degree(reference.degrees())
     }
 }
 
-fn line_direction(horizontal: LengthOrPercentage, vertical: LengthOrPercentage) -> LineDirection {
+fn line_direction(horizontal: LengthPercentage, vertical: LengthPercentage) -> LineDirection {
     use crate::values::computed::position::Position;
     use crate::values::specified::position::{X, Y};
 
-    let horizontal_percentage = match horizontal {
-        LengthOrPercentage::Percentage(percentage) => Some(percentage.0),
-        _ => None,
-    };
-
-    let vertical_percentage = match vertical {
-        LengthOrPercentage::Percentage(percentage) => Some(percentage.0),
-        _ => None,
-    };
+    let horizontal_percentage = horizontal.as_percentage();
+    let vertical_percentage = vertical.as_percentage();
 
     let horizontal_as_corner = horizontal_percentage.and_then(|percentage| {
-        if percentage == 0.0 {
+        if percentage.0 == 0.0 {
             Some(X::Left)
-        } else if percentage == 1.0 {
+        } else if percentage.0 == 1.0 {
             Some(X::Right)
         } else {
             None
         }
     });
 
     let vertical_as_corner = vertical_percentage.and_then(|percentage| {
-        if percentage == 0.0 {
+        if percentage.0 == 0.0 {
             Some(Y::Top)
-        } else if percentage == 1.0 {
+        } else if percentage.0 == 1.0 {
             Some(Y::Bottom)
         } else {
             None
         }
     });
 
     if let (Some(hc), Some(vc)) = (horizontal_as_corner, vertical_as_corner) {
         return LineDirection::Corner(hc, vc);
     }
 
     if let Some(hc) = horizontal_as_corner {
-        if vertical_percentage == Some(0.5) {
+        if vertical_percentage == Some(Percentage(0.5)) {
             return LineDirection::Horizontal(hc);
         }
     }
 
     if let Some(vc) = vertical_as_corner {
-        if horizontal_percentage == Some(0.5) {
+        if horizontal_percentage == Some(Percentage(0.5)) {
             return LineDirection::Vertical(vc);
         }
     }
 
     LineDirection::MozPosition(
         Some(Position {
             horizontal,
             vertical,
@@ -507,18 +462,18 @@ impl nsStyleImage {
         use crate::values::computed::Length;
         use crate::values::generics::image::{Circle, ColorStop, CompatMode, Ellipse};
         use crate::values::generics::image::{EndingShape, GradientKind, ShapeExtent};
 
         let gecko_gradient = bindings::Gecko_GetGradientImageValue(self)
             .as_ref()
             .unwrap();
         let angle = Angle::from_gecko_style_coord(&gecko_gradient.mAngle);
-        let horizontal_style = LengthOrPercentage::from_gecko_style_coord(&gecko_gradient.mBgPosX);
-        let vertical_style = LengthOrPercentage::from_gecko_style_coord(&gecko_gradient.mBgPosY);
+        let horizontal_style = LengthPercentage::from_gecko_style_coord(&gecko_gradient.mBgPosX);
+        let vertical_style = LengthPercentage::from_gecko_style_coord(&gecko_gradient.mBgPosY);
 
         let kind = match gecko_gradient.mShape as u32 {
             structs::NS_STYLE_GRADIENT_SHAPE_LINEAR => {
                 let line_direction = match (angle, horizontal_style, vertical_style) {
                     (Some(a), None, None) => LineDirection::Angle(a),
                     (None, Some(horizontal), Some(vertical)) => {
                         line_direction(horizontal, vertical)
                     },
@@ -569,30 +524,30 @@ impl nsStyleImage {
                             },
                             size => Circle::Extent(gecko_size_to_keyword(size)),
                         };
                         EndingShape::Circle(circle)
                     },
                     structs::NS_STYLE_GRADIENT_SHAPE_ELLIPTICAL => {
                         let length_percentage_keyword = match gecko_gradient.mSize as u32 {
                             structs::NS_STYLE_GRADIENT_SIZE_EXPLICIT_SIZE => match (
-                                LengthOrPercentage::from_gecko_style_coord(
+                                LengthPercentage::from_gecko_style_coord(
                                     &gecko_gradient.mRadiusX,
                                 ),
-                                LengthOrPercentage::from_gecko_style_coord(
+                                LengthPercentage::from_gecko_style_coord(
                                     &gecko_gradient.mRadiusY,
                                 ),
                             ) {
                                 (Some(x), Some(y)) => Ellipse::Radii(x, y),
                                 _ => {
                                     debug_assert!(false,
-                                                      "mRadiusX, mRadiusY could not convert to LengthOrPercentage");
+                                                      "mRadiusX, mRadiusY could not convert to LengthPercentage");
                                     Ellipse::Radii(
-                                        LengthOrPercentage::zero(),
-                                        LengthOrPercentage::zero(),
+                                        LengthPercentage::zero(),
+                                        LengthPercentage::zero(),
                                     )
                                 },
                             },
                             size => Ellipse::Extent(gecko_size_to_keyword(size)),
                         };
                         EndingShape::Ellipse(length_percentage_keyword)
                     },
                     _ => panic!("Found unexpected mShape"),
@@ -601,42 +556,42 @@ impl nsStyleImage {
                 let position = match (horizontal_style, vertical_style) {
                     (Some(horizontal), Some(vertical)) => Position {
                         horizontal,
                         vertical,
                     },
                     _ => {
                         debug_assert!(
                             false,
-                            "mRadiusX, mRadiusY could not convert to LengthOrPercentage"
+                            "mRadiusX, mRadiusY could not convert to LengthPercentage"
                         );
                         Position {
-                            horizontal: LengthOrPercentage::zero(),
-                            vertical: LengthOrPercentage::zero(),
+                            horizontal: LengthPercentage::zero(),
+                            vertical: LengthPercentage::zero(),
                         }
                     },
                 };
 
                 GradientKind::Radial(shape, position, angle)
             },
         };
 
         let items = gecko_gradient
             .mStops
             .iter()
             .map(|ref stop| {
                 if stop.mIsInterpolationHint {
                     GradientItem::InterpolationHint(
-                        LengthOrPercentage::from_gecko_style_coord(&stop.mLocation)
-                            .expect("mLocation could not convert to LengthOrPercentage"),
+                        LengthPercentage::from_gecko_style_coord(&stop.mLocation)
+                            .expect("mLocation could not convert to LengthPercentage"),
                     )
                 } else {
                     GradientItem::ColorStop(ColorStop {
                         color: stop.mColor.into(),
-                        position: LengthOrPercentage::from_gecko_style_coord(&stop.mLocation),
+                        position: LengthPercentage::from_gecko_style_coord(&stop.mLocation),
                     })
                 }
             })
             .collect();
 
         let compat_mode = if gecko_gradient.mMozLegacySyntax {
             CompatMode::Moz
         } else if gecko_gradient.mLegacySyntax {
@@ -665,17 +620,17 @@ pub mod basic_shape {
         StyleGeometryBox, StyleShapeSource, StyleShapeSourceType,
     };
     use crate::gecko_bindings::sugar::ns_style_coord::{CoordDataMut, CoordDataValue};
     use crate::gecko_bindings::sugar::refptr::RefPtr;
     use crate::values::computed::basic_shape::{
         BasicShape, ClippingShape, FloatAreaShape, ShapeRadius,
     };
     use crate::values::computed::border::{BorderCornerRadius, BorderRadius};
-    use crate::values::computed::length::LengthOrPercentage;
+    use crate::values::computed::length::LengthPercentage;
     use crate::values::computed::motion::OffsetPath;
     use crate::values::computed::position;
     use crate::values::computed::url::ComputedUrl;
     use crate::values::generics::basic_shape::{
         BasicShape as GenericBasicShape, InsetRect, Polygon,
     };
     use crate::values::generics::basic_shape::{Circle, Ellipse, Path, PolygonCoord};
     use crate::values::generics::basic_shape::{GeometryBox, ShapeBox, ShapeSource};
@@ -782,20 +737,20 @@ pub mod basic_shape {
             }
         }
     }
 
     impl<'a> From<&'a StyleBasicShape> for BasicShape {
         fn from(other: &'a StyleBasicShape) -> Self {
             match other.mType {
                 StyleBasicShapeType::Inset => {
-                    let t = LengthOrPercentage::from_gecko_style_coord(&other.mCoordinates[0]);
-                    let r = LengthOrPercentage::from_gecko_style_coord(&other.mCoordinates[1]);
-                    let b = LengthOrPercentage::from_gecko_style_coord(&other.mCoordinates[2]);
-                    let l = LengthOrPercentage::from_gecko_style_coord(&other.mCoordinates[3]);
+                    let t = LengthPercentage::from_gecko_style_coord(&other.mCoordinates[0]);
+                    let r = LengthPercentage::from_gecko_style_coord(&other.mCoordinates[1]);
+                    let b = LengthPercentage::from_gecko_style_coord(&other.mCoordinates[2]);
+                    let l = LengthPercentage::from_gecko_style_coord(&other.mCoordinates[3]);
                     let round: BorderRadius = (&other.mRadius).into();
                     let round = if round.all_zero() { None } else { Some(round) };
                     let rect = Rect::new(
                         t.expect("inset() offset should be a length, percentage, or calc value"),
                         r.expect("inset() offset should be a length, percentage, or calc value"),
                         b.expect("inset() offset should be a length, percentage, or calc value"),
                         l.expect("inset() offset should be a length, percentage, or calc value"),
                     );
@@ -811,22 +766,22 @@ pub mod basic_shape {
                     position: (&other.mPosition).into(),
                 }),
                 StyleBasicShapeType::Polygon => {
                     let mut coords = Vec::with_capacity(other.mCoordinates.len() / 2);
                     for i in 0..(other.mCoordinates.len() / 2) {
                         let x = 2 * i;
                         let y = x + 1;
                         coords.push(PolygonCoord(
-                            LengthOrPercentage::from_gecko_style_coord(&other.mCoordinates[x])
+                            LengthPercentage::from_gecko_style_coord(&other.mCoordinates[x])
                                 .expect(
                                     "polygon() coordinate should be a length, percentage, \
                                      or calc value",
                                 ),
-                            LengthOrPercentage::from_gecko_style_coord(&other.mCoordinates[y])
+                            LengthPercentage::from_gecko_style_coord(&other.mCoordinates[y])
                                 .expect(
                                     "polygon() coordinate should be a length, percentage, \
                                      or calc value",
                                 ),
                         ))
                     }
                     GenericBasicShape::Polygon(Polygon {
                         fill: other.mFillRule,
@@ -837,22 +792,22 @@ pub mod basic_shape {
         }
     }
 
     impl<'a> From<&'a nsStyleCorners> for BorderRadius {
         fn from(other: &'a nsStyleCorners) -> Self {
             let get_corner = |index| {
                 BorderCornerRadius::new(
                     NonNegative(
-                        LengthOrPercentage::from_gecko_style_coord(&other.data_at(index)).expect(
+                        LengthPercentage::from_gecko_style_coord(&other.data_at(index)).expect(
                             "<border-radius> should be a length, percentage, or calc value",
                         ),
                     ),
                     NonNegative(
-                        LengthOrPercentage::from_gecko_style_coord(&other.data_at(index + 1))
+                        LengthPercentage::from_gecko_style_coord(&other.data_at(index + 1))
                             .expect(
                                 "<border-radius> should be a length, percentage, or calc value",
                             ),
                     ),
                 )
             };
 
             GenericBorderRadius {
@@ -998,32 +953,32 @@ impl From<Origin> for SheetType {
         match other {
             Origin::UserAgent => SheetType::Agent,
             Origin::Author => SheetType::Doc,
             Origin::User => SheetType::User,
         }
     }
 }
 
-impl TrackSize<LengthOrPercentage> {
+impl TrackSize<LengthPercentage> {
     /// Return TrackSize from given two nsStyleCoord
     pub fn from_gecko_style_coords<T: CoordData>(gecko_min: &T, gecko_max: &T) -> Self {
         use crate::gecko_bindings::structs::root::nsStyleUnit;
-        use crate::values::computed::length::LengthOrPercentage;
+        use crate::values::computed::length::LengthPercentage;
         use crate::values::generics::grid::{TrackBreadth, TrackSize};
 
         if gecko_min.unit() == nsStyleUnit::eStyleUnit_None {
             debug_assert!(
                 gecko_max.unit() == nsStyleUnit::eStyleUnit_Coord ||
                     gecko_max.unit() == nsStyleUnit::eStyleUnit_Percent ||
                     gecko_max.unit() == nsStyleUnit::eStyleUnit_Calc
             );
             return TrackSize::FitContent(
-                LengthOrPercentage::from_gecko_style_coord(gecko_max)
-                    .expect("gecko_max could not convert to LengthOrPercentage"),
+                LengthPercentage::from_gecko_style_coord(gecko_max)
+                    .expect("gecko_max could not convert to LengthPercentage"),
             );
         }
 
         let min = TrackBreadth::from_gecko_style_coord(gecko_min)
             .expect("gecko_min could not convert to TrackBreadth");
         let max = TrackBreadth::from_gecko_style_coord(gecko_max)
             .expect("gecko_max could not convert to TrackBreadth");
         if min == max {
@@ -1053,17 +1008,17 @@ impl TrackSize<LengthOrPercentage> {
             TrackSize::Minmax(ref min, ref max) => {
                 min.to_gecko_style_coord(gecko_min);
                 max.to_gecko_style_coord(gecko_max);
             },
         }
     }
 }
 
-impl TrackListValue<LengthOrPercentage, Integer> {
+impl TrackListValue<LengthPercentage, Integer> {
     /// Return TrackSize from given two nsStyleCoord
     pub fn from_gecko_style_coords<T: CoordData>(gecko_min: &T, gecko_max: &T) -> Self {
         TrackListValue::TrackSize(TrackSize::from_gecko_style_coords(gecko_min, gecko_max))
     }
 
     /// Save TrackSize to given gecko fields.
     pub fn to_gecko_style_coords<T: CoordDataMut>(&self, gecko_min: &mut T, gecko_max: &mut T) {
         use crate::values::generics::grid::TrackListValue;
--- a/servo/components/style/gecko/values.rs
+++ b/servo/components/style/gecko/values.rs
@@ -8,19 +8,19 @@
 
 use crate::counter_style::{Symbol, Symbols};
 use crate::gecko_bindings::structs::{self, nsStyleCoord, CounterStylePtr};
 use crate::gecko_bindings::structs::{StyleGridTrackBreadth, StyleShapeRadius};
 use crate::gecko_bindings::sugar::ns_style_coord::{CoordData, CoordDataMut, CoordDataValue};
 use crate::media_queries::Device;
 use crate::values::computed::basic_shape::ShapeRadius as ComputedShapeRadius;
 use crate::values::computed::FlexBasis as ComputedFlexBasis;
-use crate::values::computed::{Angle, ExtremumLength, Length, LengthOrPercentage};
-use crate::values::computed::{LengthOrPercentageOrAuto, Percentage};
-use crate::values::computed::{LengthOrPercentageOrNone, Number, NumberOrPercentage};
+use crate::values::computed::{Angle, ExtremumLength, Length, LengthPercentage};
+use crate::values::computed::{LengthPercentageOrAuto, Percentage};
+use crate::values::computed::{LengthPercentageOrNone, Number, NumberOrPercentage};
 use crate::values::computed::{MaxLength as ComputedMaxLength, MozLength as ComputedMozLength};
 use crate::values::generics::basic_shape::ShapeRadius;
 use crate::values::generics::box_::Perspective;
 use crate::values::generics::flex::FlexBasis;
 use crate::values::generics::gecko::ScrollSnapPoint;
 use crate::values::generics::grid::{TrackBreadth, TrackKeyword};
 use crate::values::generics::length::{MaxLength, MozLength};
 use crate::values::generics::{CounterStyleOrNone, NonNegative};
@@ -141,31 +141,33 @@ impl GeckoStyleCoordConvertible for Numb
         match coord.as_value() {
             CoordDataValue::Factor(f) => Some(NumberOrPercentage::Number(f)),
             CoordDataValue::Percent(p) => Some(NumberOrPercentage::Percentage(Percentage(p))),
             _ => None,
         }
     }
 }
 
-impl GeckoStyleCoordConvertible for LengthOrPercentage {
+impl GeckoStyleCoordConvertible for LengthPercentage {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
-        let value = match *self {
-            LengthOrPercentage::Length(px) => CoordDataValue::Coord(px.to_i32_au()),
-            LengthOrPercentage::Percentage(p) => CoordDataValue::Percent(p.0),
-            LengthOrPercentage::Calc(calc) => CoordDataValue::Calc(calc.into()),
-        };
-        coord.set_value(value);
+        if self.was_calc {
+            return coord.set_value(CoordDataValue::Calc((*self).into()))
+        }
+        debug_assert!(self.percentage.is_none() || self.unclamped_length() == Length::zero());
+        if let Some(p) = self.percentage {
+            return coord.set_value(CoordDataValue::Percent(p.0));
+        }
+        coord.set_value(CoordDataValue::Coord(self.unclamped_length().to_i32_au()))
     }
 
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
         match coord.as_value() {
-            CoordDataValue::Coord(coord) => Some(LengthOrPercentage::Length(Au(coord).into())),
-            CoordDataValue::Percent(p) => Some(LengthOrPercentage::Percentage(Percentage(p))),
-            CoordDataValue::Calc(calc) => Some(LengthOrPercentage::Calc(calc.into())),
+            CoordDataValue::Coord(coord) => Some(LengthPercentage::new(Au(coord).into(), None)),
+            CoordDataValue::Percent(p) => Some(LengthPercentage::new(Au(0).into(), Some(Percentage(p)))),
+            CoordDataValue::Calc(calc) => Some(calc.into()),
             _ => None,
         }
     }
 }
 
 impl GeckoStyleCoordConvertible for Length {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
         coord.set_value(CoordDataValue::Coord(self.to_i32_au()));
@@ -174,68 +176,52 @@ impl GeckoStyleCoordConvertible for Leng
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
         match coord.as_value() {
             CoordDataValue::Coord(coord) => Some(Au(coord).into()),
             _ => None,
         }
     }
 }
 
-impl GeckoStyleCoordConvertible for LengthOrPercentageOrAuto {
+impl GeckoStyleCoordConvertible for LengthPercentageOrAuto {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
-        let value = match *self {
-            LengthOrPercentageOrAuto::Length(px) => CoordDataValue::Coord(px.to_i32_au()),
-            LengthOrPercentageOrAuto::Percentage(p) => CoordDataValue::Percent(p.0),
-            LengthOrPercentageOrAuto::Auto => CoordDataValue::Auto,
-            LengthOrPercentageOrAuto::Calc(calc) => CoordDataValue::Calc(calc.into()),
-        };
-        coord.set_value(value);
+        match *self {
+            LengthPercentageOrAuto::Auto => coord.set_value(CoordDataValue::Auto),
+            LengthPercentageOrAuto::LengthPercentage(ref lp) => lp.to_gecko_style_coord(coord),
+        }
     }
 
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
         match coord.as_value() {
-            CoordDataValue::Coord(coord) => {
-                Some(LengthOrPercentageOrAuto::Length(Au(coord).into()))
-            },
-            CoordDataValue::Percent(p) => Some(LengthOrPercentageOrAuto::Percentage(Percentage(p))),
-            CoordDataValue::Auto => Some(LengthOrPercentageOrAuto::Auto),
-            CoordDataValue::Calc(calc) => Some(LengthOrPercentageOrAuto::Calc(calc.into())),
-            _ => None,
+            CoordDataValue::Auto => Some(LengthPercentageOrAuto::Auto),
+            _ => LengthPercentage::from_gecko_style_coord(coord).map(LengthPercentageOrAuto::LengthPercentage),
         }
     }
 }
 
-impl GeckoStyleCoordConvertible for LengthOrPercentageOrNone {
+impl GeckoStyleCoordConvertible for LengthPercentageOrNone {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
-        let value = match *self {
-            LengthOrPercentageOrNone::Length(px) => CoordDataValue::Coord(px.to_i32_au()),
-            LengthOrPercentageOrNone::Percentage(p) => CoordDataValue::Percent(p.0),
-            LengthOrPercentageOrNone::None => CoordDataValue::None,
-            LengthOrPercentageOrNone::Calc(calc) => CoordDataValue::Calc(calc.into()),
-        };
-        coord.set_value(value);
+        match *self {
+            LengthPercentageOrNone::None => coord.set_value(CoordDataValue::None),
+            LengthPercentageOrNone::LengthPercentage(ref lp) => lp.to_gecko_style_coord(coord),
+        }
     }
 
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
         match coord.as_value() {
-            CoordDataValue::Coord(coord) => {
-                Some(LengthOrPercentageOrNone::Length(Au(coord).into()))
-            },
-            CoordDataValue::Percent(p) => Some(LengthOrPercentageOrNone::Percentage(Percentage(p))),
-            CoordDataValue::None => Some(LengthOrPercentageOrNone::None),
-            CoordDataValue::Calc(calc) => Some(LengthOrPercentageOrNone::Calc(calc.into())),
-            _ => None,
+            CoordDataValue::None => Some(LengthPercentageOrNone::None),
+            _ => LengthPercentage::from_gecko_style_coord(coord).map(LengthPercentageOrNone::LengthPercentage),
         }
     }
 }
 
 impl<L: GeckoStyleCoordConvertible> GeckoStyleCoordConvertible for TrackBreadth<L> {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
         match *self {
-            TrackBreadth::Breadth(ref lop) => lop.to_gecko_style_coord(coord),
+            TrackBreadth::Breadth(ref lp) => lp.to_gecko_style_coord(coord),
             TrackBreadth::Fr(fr) => coord.set_value(CoordDataValue::FlexFraction(fr)),
             TrackBreadth::Keyword(TrackKeyword::Auto) => coord.set_value(CoordDataValue::Auto),
             TrackBreadth::Keyword(TrackKeyword::MinContent) => coord.set_value(
                 CoordDataValue::Enumerated(StyleGridTrackBreadth::MinContent as u32),
             ),
             TrackBreadth::Keyword(TrackKeyword::MaxContent) => coord.set_value(
                 CoordDataValue::Enumerated(StyleGridTrackBreadth::MaxContent as u32),
             ),
@@ -266,17 +252,17 @@ impl GeckoStyleCoordConvertible for Comp
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
         match *self {
             ShapeRadius::ClosestSide => coord.set_value(CoordDataValue::Enumerated(
                 StyleShapeRadius::ClosestSide as u32,
             )),
             ShapeRadius::FarthestSide => coord.set_value(CoordDataValue::Enumerated(
                 StyleShapeRadius::FarthestSide as u32,
             )),
-            ShapeRadius::Length(lop) => lop.to_gecko_style_coord(coord),
+            ShapeRadius::Length(lp) => lp.to_gecko_style_coord(coord),
         }
     }
 
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
         match coord.as_value() {
             CoordDataValue::Enumerated(v) => {
                 if v == StyleShapeRadius::ClosestSide as u32 {
                     Some(ShapeRadius::ClosestSide)
@@ -372,64 +358,64 @@ impl GeckoStyleCoordConvertible for Extr
             _ => None,
         }
     }
 }
 
 impl GeckoStyleCoordConvertible for ComputedMozLength {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
         match *self {
-            MozLength::LengthOrPercentageOrAuto(ref lopoa) => lopoa.to_gecko_style_coord(coord),
+            MozLength::LengthPercentageOrAuto(ref lpoa) => lpoa.to_gecko_style_coord(coord),
             MozLength::ExtremumLength(ref e) => e.to_gecko_style_coord(coord),
         }
     }
 
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
-        LengthOrPercentageOrAuto::from_gecko_style_coord(coord)
-            .map(MozLength::LengthOrPercentageOrAuto)
+        LengthPercentageOrAuto::from_gecko_style_coord(coord)
+            .map(MozLength::LengthPercentageOrAuto)
             .or_else(|| {
                 ExtremumLength::from_gecko_style_coord(coord).map(MozLength::ExtremumLength)
             })
     }
 }
 
 impl GeckoStyleCoordConvertible for ComputedMaxLength {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
         match *self {
-            MaxLength::LengthOrPercentageOrNone(ref lopon) => lopon.to_gecko_style_coord(coord),
+            MaxLength::LengthPercentageOrNone(ref lpon) => lpon.to_gecko_style_coord(coord),
             MaxLength::ExtremumLength(ref e) => e.to_gecko_style_coord(coord),
         }
     }
 
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
-        LengthOrPercentageOrNone::from_gecko_style_coord(coord)
-            .map(MaxLength::LengthOrPercentageOrNone)
+        LengthPercentageOrNone::from_gecko_style_coord(coord)
+            .map(MaxLength::LengthPercentageOrNone)
             .or_else(|| {
                 ExtremumLength::from_gecko_style_coord(coord).map(MaxLength::ExtremumLength)
             })
     }
 }
 
-impl GeckoStyleCoordConvertible for ScrollSnapPoint<LengthOrPercentage> {
+impl GeckoStyleCoordConvertible for ScrollSnapPoint<LengthPercentage> {
     fn to_gecko_style_coord<T: CoordDataMut>(&self, coord: &mut T) {
         match self.repeated() {
             None => coord.set_value(CoordDataValue::None),
             Some(l) => l.to_gecko_style_coord(coord),
         };
     }
 
     fn from_gecko_style_coord<T: CoordData>(coord: &T) -> Option<Self> {
         use crate::gecko_bindings::structs::root::nsStyleUnit;
         use crate::values::generics::gecko::ScrollSnapPoint;
 
         Some(match coord.unit() {
             nsStyleUnit::eStyleUnit_None => ScrollSnapPoint::None,
             _ => ScrollSnapPoint::Repeat(
-                LengthOrPercentage::from_gecko_style_coord(coord)
-                    .expect("coord could not convert to LengthOrPercentage"),
+                LengthPercentage::from_gecko_style_coord(coord)
+                    .expect("coord could not convert to LengthPercentage"),
             ),
         })
     }
 }
 
 impl<L> GeckoStyleCoordConvertible for Perspective<L>
 where
     L: GeckoStyleCoordConvertible,
--- a/servo/components/style/gecko_bindings/sugar/ns_css_value.rs
+++ b/servo/components/style/gecko_bindings/sugar/ns_css_value.rs
@@ -4,17 +4,17 @@
 
 //! Little helpers for `nsCSSValue`.
 
 use crate::gecko_bindings::bindings;
 use crate::gecko_bindings::structs;
 use crate::gecko_bindings::structs::{nsCSSUnit, nsCSSValue};
 use crate::gecko_bindings::structs::{nsCSSValueList, nsCSSValue_Array};
 use crate::gecko_string_cache::Atom;
-use crate::values::computed::{Angle, Length, LengthOrPercentage, Percentage};
+use crate::values::computed::{Angle, Length, LengthPercentage, Percentage};
 use std::marker::PhantomData;
 use std::mem;
 use std::ops::{Index, IndexMut};
 use std::slice;
 
 impl nsCSSValue {
     /// Create a CSSValue with null unit, useful to be used as a return value.
     #[inline]
@@ -62,46 +62,52 @@ impl nsCSSValue {
             nsCSSUnit::eCSSUnit_Array as u32 <= self.mUnit as u32 &&
                 self.mUnit as u32 <= nsCSSUnit::eCSSUnit_Calc_Plus as u32
         );
         let array = *self.mValue.mArray.as_ref();
         debug_assert!(!array.is_null());
         &*array
     }
 
-    /// Sets LengthOrPercentage value to this nsCSSValue.
-    pub unsafe fn set_lop(&mut self, lop: LengthOrPercentage) {
-        match lop {
-            LengthOrPercentage::Length(px) => self.set_px(px.px()),
-            LengthOrPercentage::Percentage(pc) => self.set_percentage(pc.0),
-            LengthOrPercentage::Calc(calc) => bindings::Gecko_CSSValue_SetCalc(self, calc.into()),
+    /// Sets LengthPercentage value to this nsCSSValue.
+    pub unsafe fn set_length_percentage(&mut self, lp: LengthPercentage) {
+        if lp.was_calc {
+            return bindings::Gecko_CSSValue_SetCalc(self, lp.into())
         }
+        debug_assert!(lp.percentage.is_none() || lp.unclamped_length() == Length::zero());
+        if let Some(p) = lp.percentage {
+            return self.set_percentage(p.0);
+        }
+        self.set_px(lp.unclamped_length().px());
     }
 
     /// Sets a px value to this nsCSSValue.
     pub unsafe fn set_px(&mut self, px: f32) {
         bindings::Gecko_CSSValue_SetPixelLength(self, px)
     }
 
     /// Sets a percentage value to this nsCSSValue.
     pub unsafe fn set_percentage(&mut self, unit_value: f32) {
         bindings::Gecko_CSSValue_SetPercentage(self, unit_value)
     }
 
-    /// Returns LengthOrPercentage value.
-    pub unsafe fn get_lop(&self) -> LengthOrPercentage {
+    /// Returns LengthPercentage value.
+    pub unsafe fn get_length_percentage(&self) -> LengthPercentage {
         match self.mUnit {
             nsCSSUnit::eCSSUnit_Pixel => {
-                LengthOrPercentage::Length(Length::new(bindings::Gecko_CSSValue_GetNumber(self)))
+                LengthPercentage::new(
+                    Length::new(bindings::Gecko_CSSValue_GetNumber(self)),
+                    None,
+                )
             },
-            nsCSSUnit::eCSSUnit_Percent => LengthOrPercentage::Percentage(Percentage(
+            nsCSSUnit::eCSSUnit_Percent => LengthPercentage::new_percent(Percentage(
                 bindings::Gecko_CSSValue_GetPercentage(self),
             )),
             nsCSSUnit::eCSSUnit_Calc => {
-                LengthOrPercentage::Calc(bindings::Gecko_CSSValue_GetCalc(self).into())
+                bindings::Gecko_CSSValue_GetCalc(self).into()
             },
             _ => panic!("Unexpected unit"),
         }
     }
 
     /// Returns Length  value.
     pub unsafe fn get_length(&self) -> Length {
         match self.mUnit {
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -505,35 +505,35 @@ def set_gecko_property(ffi_name, expr):
     }
 </%def>
 
 <%def name="impl_svg_length(ident, gecko_ffi_name)">
     // When context-value is used on an SVG length, the corresponding flag is
     // set on mContextFlags, and the length field is set to the initial value.
 
     pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) {
-        use crate::values::generics::svg::{SVGLength, SvgLengthOrPercentageOrNumber};
+        use crate::values::generics::svg::{SVGLength, SvgLengthPercentageOrNumber};
         use crate::gecko_bindings::structs::nsStyleSVG_${ident.upper()}_CONTEXT as CONTEXT_VALUE;
         let length = match v {
             SVGLength::Length(length) => {
                 self.gecko.mContextFlags &= !CONTEXT_VALUE;
                 length
             }
             SVGLength::ContextValue => {
                 self.gecko.mContextFlags |= CONTEXT_VALUE;
                 match longhands::${ident}::get_initial_value() {
                     SVGLength::Length(length) => length,
                     _ => unreachable!("Initial value should not be context-value"),
                 }
             }
         };
         match length {
-            SvgLengthOrPercentageOrNumber::LengthOrPercentage(lop) =>
-                self.gecko.${gecko_ffi_name}.set(lop),
-            SvgLengthOrPercentageOrNumber::Number(num) =>
+            SvgLengthPercentageOrNumber::LengthPercentage(lp) =>
+                self.gecko.${gecko_ffi_name}.set(lp),
+            SvgLengthPercentageOrNumber::Number(num) =>
                 self.gecko.${gecko_ffi_name}.set_value(CoordDataValue::Factor(num.into())),
         }
     }
 
     pub fn copy_${ident}_from(&mut self, other: &Self) {
         use crate::gecko_bindings::structs::nsStyleSVG_${ident.upper()}_CONTEXT as CONTEXT_VALUE;
         self.gecko.${gecko_ffi_name}.copy_from(&other.gecko.${gecko_ffi_name});
         self.gecko.mContextFlags =
@@ -541,40 +541,38 @@ def set_gecko_property(ffi_name, expr):
             (other.gecko.mContextFlags & CONTEXT_VALUE);
     }
 
     pub fn reset_${ident}(&mut self, other: &Self) {
         self.copy_${ident}_from(other)
     }
 
     pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T {
-        use crate::values::generics::svg::{SVGLength, SvgLengthOrPercentageOrNumber};
-        use crate::values::computed::LengthOrPercentage;
+        use crate::values::generics::svg::{SVGLength, SvgLengthPercentageOrNumber};
+        use crate::values::computed::LengthPercentage;
         use crate::gecko_bindings::structs::nsStyleSVG_${ident.upper()}_CONTEXT as CONTEXT_VALUE;
         if (self.gecko.mContextFlags & CONTEXT_VALUE) != 0 {
             return SVGLength::ContextValue;
         }
         let length = match self.gecko.${gecko_ffi_name}.as_value() {
             CoordDataValue::Factor(number) => {
-                SvgLengthOrPercentageOrNumber::Number(number)
+                SvgLengthPercentageOrNumber::Number(number)
             },
             CoordDataValue::Coord(coord) => {
-                SvgLengthOrPercentageOrNumber::LengthOrPercentage(
-                    LengthOrPercentage::Length(Au(coord).into())
+                SvgLengthPercentageOrNumber::LengthPercentage(
+                    LengthPercentage::new(Au(coord).into(), None)
                 )
             },
             CoordDataValue::Percent(p) => {
-                SvgLengthOrPercentageOrNumber::LengthOrPercentage(
-                    LengthOrPercentage::Percentage(Percentage(p))
+                SvgLengthPercentageOrNumber::LengthPercentage(
+                    LengthPercentage::new(Au(0).into(), Some(Percentage(p)))
                 )
             },
             CoordDataValue::Calc(calc) => {
-                SvgLengthOrPercentageOrNumber::LengthOrPercentage(
-                    LengthOrPercentage::Calc(calc.into())
-                )
+                SvgLengthPercentageOrNumber::LengthPercentage(calc.into())
             },
             _ => unreachable!("Unexpected coordinate in ${ident}"),
         };
         SVGLength::Length(length.into())
     }
 </%def>
 
 <%def name="impl_svg_opacity(ident, gecko_ffi_name)">
@@ -936,20 +934,20 @@ def set_gecko_property(ffi_name, expr):
         })
     }
 </%def>
 
 <%
 transform_functions = [
     ("Matrix3D", "matrix3d", ["number"] * 16),
     ("Matrix", "matrix", ["number"] * 6),
-    ("Translate", "translate", ["lop", "optional_lop"]),
-    ("Translate3D", "translate3d", ["lop", "lop", "length"]),
-    ("TranslateX", "translatex", ["lop"]),
-    ("TranslateY", "translatey", ["lop"]),
+    ("Translate", "translate", ["lp", "optional_lp"]),
+    ("Translate3D", "translate3d", ["lp", "lp", "length"]),
+    ("TranslateX", "translatex", ["lp"]),
+    ("TranslateY", "translatey", ["lp"]),
     ("TranslateZ", "translatez", ["length"]),
     ("Scale3D", "scale3d", ["number"] * 3),
     ("Scale", "scale", ["number", "optional_number"]),
     ("ScaleX", "scalex", ["number"]),
     ("ScaleY", "scaley", ["number"]),
     ("ScaleZ", "scalez", ["number"]),
     ("Rotate", "rotate", ["angle"]),
     ("Rotate3D", "rotate3d", ["number"] * 3 + ["angle"]),
@@ -990,17 +988,17 @@ transform_functions = [
         # First %s substituted with the call to GetArrayItem, the second
         # %s substituted with the corresponding variable
         css_value_setters = {
             "length" : "bindings::Gecko_CSSValue_SetPixelLength(%s, %s.px())",
             "percentage" : "bindings::Gecko_CSSValue_SetPercentage(%s, %s.0)",
             # Note: This is an integer type, but we use it as a percentage value in Gecko, so
             #       need to cast it to f32.
             "integer_to_percentage" : "bindings::Gecko_CSSValue_SetPercentage(%s, %s as f32)",
-            "lop" : "%s.set_lop(%s)",
+            "lp" : "%s.set_length_percentage(%s)",
             "angle" : "%s.set_angle(%s)",
             "number" : "bindings::Gecko_CSSValue_SetNumber(%s, %s)",
             # Note: We use nsCSSValueSharedList here, instead of nsCSSValueList_heap
             #       because this function is not called on the main thread and
             #       nsCSSValueList_heap is not thread safe.
             "list" : "%s.set_shared_list(%s.0.iter().map(&convert_to_ns_css_value));",
         }
     %>
@@ -1039,18 +1037,18 @@ transform_functions = [
     }
 </%def>
 
 <%def name="computed_operation_arm(name, keyword, items)">
     <%
         # %s is substituted with the call to GetArrayItem.
         css_value_getters = {
             "length" : "Length::new(bindings::Gecko_CSSValue_GetNumber(%s))",
-            "lop" : "%s.get_lop()",
-            "lopon" : "Either::Second(%s.get_lop())",
+            "lp" : "%s.get_length_percentage()",
+            "lpon" : "Either::Second(%s.get_length_percentage())",
             "lon" : "Either::First(%s.get_length())",
             "angle" : "%s.get_angle()",
             "number" : "bindings::Gecko_CSSValue_GetNumber(%s)",
             "percentage" : "Percentage(bindings::Gecko_CSSValue_GetPercentage(%s))",
             "integer_to_percentage" : "bindings::Gecko_CSSValue_GetPercentage(%s) as i32",
             "list" : "Transform(convert_shared_list_to_operations(%s))",
         }
         pre_symbols = "("
@@ -1266,22 +1264,22 @@ pub fn clone_transform_from_list(
 
     #[allow(non_snake_case)]
     pub fn reset_${ident}(&mut self, other: &Self) {
         self.copy_${ident}_from(other)
     }
 
     #[allow(non_snake_case)]
     pub fn clone_${ident}(&self) -> values::computed::TransformOrigin {
-        use crate::values::computed::{Length, LengthOrPercentage, TransformOrigin};
+        use crate::values::computed::{Length, LengthPercentage, TransformOrigin};
         TransformOrigin {
-            horizontal: LengthOrPercentage::from_gecko_style_coord(&self.gecko.${gecko_ffi_name}[0])
-                .expect("clone for LengthOrPercentage failed"),
-            vertical: LengthOrPercentage::from_gecko_style_coord(&self.gecko.${gecko_ffi_name}[1])
-                .expect("clone for LengthOrPercentage failed"),
+            horizontal: LengthPercentage::from_gecko_style_coord(&self.gecko.${gecko_ffi_name}[0])
+                .expect("clone for LengthPercentage failed"),
+            vertical: LengthPercentage::from_gecko_style_coord(&self.gecko.${gecko_ffi_name}[1])
+                .expect("clone for LengthPercentage failed"),
             depth: if let Some(third) = self.gecko.${gecko_ffi_name}.get(2) {
                 Length::from_gecko_style_coord(third)
                     .expect("clone for Length failed")
             } else {
                 Length::new(0.)
             },
         }
     }
@@ -1399,29 +1397,29 @@ impl Clone for ${style_struct.gecko_stru
         "Resize": impl_simple,
         "Color": impl_color,
         "ColorOrAuto": impl_color,
         "GreaterThanOrEqualToOneNumber": impl_simple,
         "Integer": impl_simple,
         "length::LengthOrAuto": impl_style_coord,
         "length::LengthOrNormal": impl_style_coord,
         "length::NonNegativeLengthOrAuto": impl_style_coord,
-        "length::NonNegativeLengthOrPercentageOrNormal": impl_style_coord,
+        "length::NonNegativeLengthPercentageOrNormal": impl_style_coord,
         "FillRule": impl_simple,
         "FlexBasis": impl_style_coord,
         "Length": impl_absolute_length,
         "LengthOrNormal": impl_style_coord,
-        "LengthOrPercentage": impl_style_coord,
-        "LengthOrPercentageOrAuto": impl_style_coord,
-        "LengthOrPercentageOrNone": impl_style_coord,
+        "LengthPercentage": impl_style_coord,
+        "LengthPercentageOrAuto": impl_style_coord,
+        "LengthPercentageOrNone": impl_style_coord,
         "MaxLength": impl_style_coord,
         "MozLength": impl_style_coord,
         "MozScriptMinSize": impl_absolute_length,
         "MozScriptSizeMultiplier": impl_simple,
-        "NonNegativeLengthOrPercentage": impl_style_coord,
+        "NonNegativeLengthPercentage": impl_style_coord,
         "NonNegativeNumber": impl_simple,
         "Number": impl_simple,
         "Opacity": impl_simple,
         "OverflowWrap": impl_simple,
         "Perspective": impl_style_coord,
         "Position": impl_position,
         "RGBAColor": impl_rgba_color,
         "SVGLength": impl_svg_length,
@@ -3081,25 +3079,25 @@ fn static_assert() {
                 self.gecko.mVerticalAlign.set(length);
                 return;
             },
         };
         self.gecko.mVerticalAlign.set_value(CoordDataValue::Enumerated(value));
     }
 
     pub fn clone_vertical_align(&self) -> longhands::vertical_align::computed_value::T {
-        use crate::values::computed::LengthOrPercentage;
+        use crate::values::computed::LengthPercentage;
         use crate::values::generics::box_::VerticalAlign;
 
         let gecko = &self.gecko.mVerticalAlign;
         match gecko.as_value() {
             CoordDataValue::Enumerated(value) => VerticalAlign::from_gecko_keyword(value),
             _ => {
                 VerticalAlign::Length(
-                    LengthOrPercentage::from_gecko_style_coord(gecko).expect(
+                    LengthPercentage::from_gecko_style_coord(gecko).expect(
                         "expected <length-percentage> for vertical-align",
                     ),
                 )
             },
         }
     }
 
     <%call expr="impl_coord_copy('vertical_align', 'mVerticalAlign')"></%call>
@@ -3383,21 +3381,21 @@ fn static_assert() {
     }
 
     pub fn reset_perspective_origin(&mut self, other: &Self) {
         self.copy_perspective_origin_from(other)
     }
 
     pub fn clone_perspective_origin(&self) -> longhands::perspective_origin::computed_value::T {
         use crate::properties::longhands::perspective_origin::computed_value::T;
-        use crate::values::computed::LengthOrPercentage;
+        use crate::values::computed::LengthPercentage;
         T {
-            horizontal: LengthOrPercentage::from_gecko_style_coord(&self.gecko.mPerspectiveOrigin[0])
+            horizontal: LengthPercentage::from_gecko_style_coord(&self.gecko.mPerspectiveOrigin[0])
                 .expect("Expected length or percentage for horizontal value of perspective-origin"),
-            vertical: LengthOrPercentage::from_gecko_style_coord(&self.gecko.mPerspectiveOrigin[1])
+            vertical: LengthPercentage::from_gecko_style_coord(&self.gecko.mPerspectiveOrigin[1])
                 .expect("Expected length or percentage for vertical value of perspective-origin"),
         }
     }
 
     ${impl_individual_transform('rotate', 'Rotate', 'mSpecifiedRotate')}
     ${impl_individual_transform('translate', 'Translate', 'mSpecifiedTranslate')}
     ${impl_individual_transform('scale', 'Scale', 'mSpecifiedScale')}
 
@@ -3876,22 +3874,22 @@ fn static_assert() {
             mWidthType: w_type as u8,
             mHeightType: h_type as u8,
         }
     </%self:simple_image_array_property>
 
     pub fn clone_${shorthand}_size(&self) -> longhands::${shorthand}_size::computed_value::T {
         use crate::gecko_bindings::structs::nsStyleCoord_CalcValue as CalcValue;
         use crate::gecko_bindings::structs::nsStyleImageLayers_Size_DimensionType as DimensionType;
-        use crate::values::computed::NonNegativeLengthOrPercentageOrAuto;
+        use crate::values::computed::NonNegativeLengthPercentageOrAuto;
         use crate::values::generics::background::BackgroundSize;
 
-        fn to_servo(value: CalcValue, ty: u8) -> NonNegativeLengthOrPercentageOrAuto {
+        fn to_servo(value: CalcValue, ty: u8) -> NonNegativeLengthPercentageOrAuto {
             if ty == DimensionType::eAuto as u8 {
-                NonNegativeLengthOrPercentageOrAuto::auto()
+                NonNegativeLengthPercentageOrAuto::auto()
             } else {
                 debug_assert_eq!(ty, DimensionType::eLengthPercentage as u8);
                 value.into()
             }
         }
 
         longhands::${shorthand}_size::computed_value::List(
             self.gecko.${image_layers_field}.mLayers.iter().map(|ref layer| {
@@ -4565,33 +4563,33 @@ fn static_assert() {
         Length::from_gecko_style_coord(&self.gecko.mLetterSpacing).map_or(Spacing::Normal, Spacing::Value)
     }
 
     <%call expr="impl_coord_copy('letter_spacing', 'mLetterSpacing')"></%call>
 
     pub fn set_word_spacing(&mut self, v: longhands::word_spacing::computed_value::T) {
         use crate::values::generics::text::Spacing;
         match v {
-            Spacing::Value(lop) => self.gecko.mWordSpacing.set(lop),
+            Spacing::Value(lp) => self.gecko.mWordSpacing.set(lp),
             // https://drafts.csswg.org/css-text-3/#valdef-word-spacing-normal
             Spacing::Normal => self.gecko.mWordSpacing.set_value(CoordDataValue::Coord(0)),
         }
     }
 
     pub fn clone_word_spacing(&self) -> longhands::word_spacing::computed_value::T {
-        use crate::values::computed::LengthOrPercentage;
+        use crate::values::computed::LengthPercentage;
         use crate::values::generics::text::Spacing;
         debug_assert!(
             matches!(self.gecko.mWordSpacing.as_value(),
                      CoordDataValue::Normal |
                      CoordDataValue::Coord(_) |
                      CoordDataValue::Percent(_) |
                      CoordDataValue::Calc(_)),
             "Unexpected computed value for word-spacing");
-        LengthOrPercentage::from_gecko_style_coord(&self.gecko.mWordSpacing).map_or(Spacing::Normal, Spacing::Value)
+        LengthPercentage::from_gecko_style_coord(&self.gecko.mWordSpacing).map_or(Spacing::Normal, Spacing::Value)
     }
 
     <%call expr="impl_coord_copy('word_spacing', 'mWordSpacing')"></%call>
 
     fn clear_text_emphasis_style_if_string(&mut self) {
         if self.gecko.mTextEmphasisStyle == structs::NS_STYLE_TEXT_EMPHASIS_STYLE_STRING as u8 {
             self.gecko.mTextEmphasisStyleString.truncate();
             self.gecko.mTextEmphasisStyle = structs::NS_STYLE_TEXT_EMPHASIS_STYLE_NONE as u8;
@@ -5013,30 +5011,30 @@ clip-path
 
     pub fn clone_paint_order(&self) -> longhands::paint_order::computed_value::T {
         use crate::properties::longhands::paint_order::computed_value::T;
         T(self.gecko.mPaintOrder)
     }
 
     pub fn set_stroke_dasharray(&mut self, v: longhands::stroke_dasharray::computed_value::T) {
         use crate::gecko_bindings::structs::nsStyleSVG_STROKE_DASHARRAY_CONTEXT as CONTEXT_VALUE;
-        use crate::values::generics::svg::{SVGStrokeDashArray, SvgLengthOrPercentageOrNumber};
+        use crate::values::generics::svg::{SVGStrokeDashArray, SvgLengthPercentageOrNumber};
 
         match v {
             SVGStrokeDashArray::Values(v) => {
                 let v = v.into_iter();
                 self.gecko.mContextFlags &= !CONTEXT_VALUE;
                 unsafe {
                     bindings::Gecko_nsStyleSVG_SetDashArrayLength(&mut self.gecko, v.len() as u32);
                 }
                 for (gecko, servo) in self.gecko.mStrokeDasharray.iter_mut().zip(v) {
                     match servo {
-                        SvgLengthOrPercentageOrNumber::LengthOrPercentage(lop) =>
-                            gecko.set(lop),
-                        SvgLengthOrPercentageOrNumber::Number(num) =>
+                        SvgLengthPercentageOrNumber::LengthPercentage(lp) =>
+                            gecko.set(lp),
+                        SvgLengthPercentageOrNumber::Number(num) =>
                             gecko.set_value(CoordDataValue::Factor(num.into())),
                     }
                 }
             }
             SVGStrokeDashArray::ContextValue => {
                 self.gecko.mContextFlags |= CONTEXT_VALUE;
                 unsafe {
                     bindings::Gecko_nsStyleSVG_SetDashArrayLength(&mut self.gecko, 0);
@@ -5056,37 +5054,38 @@ clip-path
     }
 
     pub fn reset_stroke_dasharray(&mut self, other: &Self) {
         self.copy_stroke_dasharray_from(other)
     }
 
     pub fn clone_stroke_dasharray(&self) -> longhands::stroke_dasharray::computed_value::T {
         use crate::gecko_bindings::structs::nsStyleSVG_STROKE_DASHARRAY_CONTEXT as CONTEXT_VALUE;
-        use crate::values::computed::LengthOrPercentage;
-        use crate::values::generics::svg::{SVGStrokeDashArray, SvgLengthOrPercentageOrNumber};
+        use crate::values::computed::LengthPercentage;
+        use crate::values::generics::NonNegative;
+        use crate::values::generics::svg::{SVGStrokeDashArray, SvgLengthPercentageOrNumber};
 
         if self.gecko.mContextFlags & CONTEXT_VALUE != 0 {
             debug_assert_eq!(self.gecko.mStrokeDasharray.len(), 0);
             return SVGStrokeDashArray::ContextValue;
         }
         let mut vec = vec![];
         for gecko in self.gecko.mStrokeDasharray.iter() {
             match gecko.as_value() {
                 CoordDataValue::Factor(number) =>
-                    vec.push(SvgLengthOrPercentageOrNumber::Number(number.into())),
+                    vec.push(SvgLengthPercentageOrNumber::Number(number.into())),
                 CoordDataValue::Coord(coord) =>
-                    vec.push(SvgLengthOrPercentageOrNumber::LengthOrPercentage(
-                        LengthOrPercentage::Length(Au(coord).into()).into())),
+                    vec.push(SvgLengthPercentageOrNumber::LengthPercentage(
+                        NonNegative(LengthPercentage::new(Au(coord).into(), None).into()))),
                 CoordDataValue::Percent(p) =>
-                    vec.push(SvgLengthOrPercentageOrNumber::LengthOrPercentage(
-                        LengthOrPercentage::Percentage(Percentage(p)).into())),
+                    vec.push(SvgLengthPercentageOrNumber::LengthPercentage(
+                        NonNegative(LengthPercentage::new_percent(Percentage(p)).into()))),
                 CoordDataValue::Calc(calc) =>
-                    vec.push(SvgLengthOrPercentageOrNumber::LengthOrPercentage(
-                        LengthOrPercentage::Calc(calc.into()).into())),
+                    vec.push(SvgLengthPercentageOrNumber::LengthPercentage(
+                        NonNegative(LengthPercentage::from(calc).clamp_to_non_negative()))),
                 _ => unreachable!(),
             }
         }
         SVGStrokeDashArray::Values(vec)
     }
 
     #[allow(non_snake_case)]
     pub fn _moz_context_properties_count(&self) -> usize {
--- a/servo/components/style/properties/longhands/background.mako.rs
+++ b/servo/components/style/properties/longhands/background.mako.rs
@@ -30,17 +30,17 @@
     ignored_when_colors_disabled="True",
     flags="APPLIES_TO_FIRST_LETTER APPLIES_TO_FIRST_LINE APPLIES_TO_PLACEHOLDER",
 )}
 
 % for (axis, direction, initial) in [("x", "Horizontal", "left"), ("y", "Vertical", "top")]:
     ${helpers.predefined_type(
         "background-position-" + axis,
         "position::" + direction + "Position",
-        initial_value="computed::LengthOrPercentage::zero()",
+        initial_value="computed::LengthPercentage::zero()",
         initial_specified_value="SpecifiedValue::initial_specified_value()",
         spec="https://drafts.csswg.org/css-backgrounds-4/#propdef-background-position-" + axis,
         animation_value_type="ComputedValue",
         vector=True,
         vector_animation_type="repeatable_list",
         flags="APPLIES_TO_FIRST_LETTER APPLIES_TO_FIRST_LINE APPLIES_TO_PLACEHOLDER",
     )}
 % endfor
--- a/servo/components/style/properties/longhands/border.mako.rs
+++ b/servo/components/style/properties/longhands/border.mako.rs
@@ -64,17 +64,17 @@
 ${helpers.gecko_keyword_conversion(
     Keyword('border-style',
     "none solid double dotted dashed hidden groove ridge inset outset",
     gecko_enum_prefix="StyleBorderStyle",
     gecko_inexhaustive=True),
     type="crate::values::specified::BorderStyle",
 )}
 
-// FIXME(#4126): when gfx supports painting it, make this Size2D<LengthOrPercentage>
+// FIXME(#4126): when gfx supports painting it, make this Size2D<LengthPercentage>
 % for corner in ["top-left", "top-right", "bottom-right", "bottom-left"]:
     ${helpers.predefined_type(
         "border-" + corner + "-radius",
         "BorderCornerRadius",
         "computed::BorderCornerRadius::zero()",
         "parse",
         extra_prefixes="webkit",
         spec="https://drafts.csswg.org/css-backgrounds/#border-%s-radius" % corner,
@@ -184,17 +184,17 @@ impl crate::values::computed::BorderImag
     }
 
     pub fn from_gecko_rect(
         sides: &crate::gecko_bindings::structs::nsStyleSides,
     ) -> Option<crate::values::computed::BorderImageWidth> {
         use crate::gecko_bindings::structs::nsStyleUnit::{eStyleUnit_Factor, eStyleUnit_Auto};
         use crate::gecko_bindings::sugar::ns_style_coord::CoordData;
         use crate::gecko::values::GeckoStyleCoordConvertible;
-        use crate::values::computed::{LengthOrPercentage, Number};
+        use crate::values::computed::{LengthPercentage, Number};
         use crate::values::generics::border::BorderImageSideWidth;
         use crate::values::generics::NonNegative;
 
         Some(
             crate::values::computed::BorderImageWidth::new(
                 % for i in range(0, 4):
                 match sides.data_at(${i}).unit() {
                     eStyleUnit_Auto => {
@@ -202,17 +202,17 @@ impl crate::values::computed::BorderImag
                     },
                     eStyleUnit_Factor => {
                         BorderImageSideWidth::Number(
                             NonNegative(Number::from_gecko_style_coord(&sides.data_at(${i}))
                                 .expect("sides[${i}] could not convert to Number")))
                     },
                     _ => {
                         BorderImageSideWidth::Length(
-                            NonNegative(LengthOrPercentage::from_gecko_style_coord(&sides.data_at(${i}))
-                                .expect("sides[${i}] could not convert to LengthOrPercentage")))
+                            NonNegative(LengthPercentage::from_gecko_style_coord(&sides.data_at(${i}))
+                                .expect("sides[${i}] could not convert to LengthPercentage")))
                     },
                 },
                 % endfor
             )
         )
     }
 }
--- a/servo/components/style/properties/longhands/box.mako.rs
+++ b/servo/components/style/properties/longhands/box.mako.rs
@@ -606,20 +606,20 @@
     products="gecko",
     animation_value_type="ComputedValue",
     flags="APPLIES_TO_FIRST_LETTER",
     spec="https://drafts.csswg.org/css-shapes/#shape-image-threshold-property",
 )}
 
 ${helpers.predefined_type(
     "shape-margin",
-    "NonNegativeLengthOrPercentage",
-    "computed::NonNegativeLengthOrPercentage::zero()",
+    "NonNegativeLengthPercentage",
+    "computed::NonNegativeLengthPercentage::zero()",
     products="gecko",
-    animation_value_type="NonNegativeLengthOrPercentage",
+    animation_value_type="NonNegativeLengthPercentage",
     flags="APPLIES_TO_FIRST_LETTER",
     spec="https://drafts.csswg.org/css-shapes/#shape-margin-property",
 )}
 
 ${helpers.predefined_type(
     "shape-outside",
     "basic_shape::FloatAreaShape",
     "generics::basic_shape::ShapeSource::None",
--- a/servo/components/style/properties/longhands/inherited_text.mako.rs
+++ b/servo/components/style/properties/longhands/inherited_text.mako.rs
@@ -48,18 +48,18 @@
     gecko_ffi_name="mTextSizeAdjust",
     products="gecko", animation_value_type="discrete",
     spec="https://drafts.csswg.org/css-size-adjust/#adjustment-control",
     alias="-webkit-text-size-adjust",
 )}
 
 ${helpers.predefined_type(
     "text-indent",
-    "LengthOrPercentage",
-    "computed::LengthOrPercentage::Length(computed::Length::new(0.))",
+    "LengthPercentage",
+    "computed::LengthPercentage::zero()",
     animation_value_type="ComputedValue",
     spec="https://drafts.csswg.org/css-text/#propdef-text-indent",
     allow_quirks=True,
     servo_restyle_damage = "reflow",
 )}
 
 // Also known as "word-wrap" (which is more popular because of IE), but this is
 // the preferred name per CSS-TEXT 6.2.
--- a/servo/components/style/properties/longhands/margin.mako.rs
+++ b/servo/components/style/properties/longhands/margin.mako.rs
@@ -9,18 +9,18 @@
 % for side in ALL_SIDES:
     <%
         spec = "https://drafts.csswg.org/css-box/#propdef-margin-%s" % side[0]
         if side[1]:
             spec = "https://drafts.csswg.org/css-logical-props/#pro