Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Fri, 11 Nov 2016 14:11:20 -0800
changeset 322305 d6a3e31a3deb77ec32d73e0ce336327a803e4873
parent 322304 b2f9a7cdcb514067018f23711c44f0906ed0e6d5 (current diff)
parent 322171 fc104971a4db41e38808e6412bc32e1900172f14 (diff)
child 322306 556c13784a9c6704cc7a6a4fa9bda2d44ada96d8
push id30945
push usercbook@mozilla.com
push dateMon, 14 Nov 2016 09:22:29 +0000
treeherdermozilla-central@1196bf3032e1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone52.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 m-c to inbound, a=merge
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -478,17 +478,25 @@ toolbar:not(#TabsToolbar) > #personal-bo
 }
 #PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-action {
   display: none;
 }
 #PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-type-icon {
   list-style-image: none;
 }
 
-#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
+#urlbar:not([actiontype="switchtab"]):not([actiontype="extension"]) > #urlbar-display-box {
+  display: none;
+}
+
+#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > #switchtab {
+  display: none;
+}
+
+#urlbar:not([actiontype="extension"]) > #urlbar-display-box > #extension {
   display: none;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem > .ac-type-icon,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-tags,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-separator,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-url {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -755,17 +755,18 @@
                 <image id="tracking-protection-icon"/>
                 <image id="connection-icon"/>
                 <hbox id="identity-icon-labels">
                   <label id="identity-icon-label" class="plain" flex="1"/>
                   <label id="identity-icon-country-label" class="plain"/>
                 </hbox>
               </box>
               <box id="urlbar-display-box" align="center">
-                <label class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
+                <label id="switchtab" class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
+                <label id="extension" class="urlbar-display urlbar-display-extension" value="&urlbar.extension.label;"/>
               </box>
               <hbox id="urlbar-icons">
                 <image id="page-report-button"
                        class="urlbar-icon"
                        hidden="true"
                        tooltiptext="&pageReportIcon.tooltip;"
                        onmousedown="gPopupBlockerObserver.onReportButtonMousedown(event);"/>
                 <image id="reader-mode-button"
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -51,16 +51,21 @@ file, You can obtain one at http://mozil
                     class="autocomplete-result-popupset"/>
       <children includes="toolbarbutton"/>
     </content>
 
     <implementation implements="nsIObserver, nsIDOMEventListener">
       <field name="AppConstants" readonly="true">
         (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
       </field>
+
+      <field name="ExtensionSearchHandler" readonly="true">
+        (Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
+      </field>
+
       <constructor><![CDATA[
         this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                                 .getService(Components.interfaces.nsIPrefService)
                                 .getBranch("browser.urlbar.");
 
         this._prefs.addObserver("", this, false);
         this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
         this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
@@ -169,16 +174,20 @@ file, You can obtain one at http://mozil
                 returnValue = action.params.displayUrl;
                 break;
               }
               case "keyword": // Fall through.
               case "searchengine": {
                 returnValue = action.params.input;
                 break;
               }
+              case "extension": {
+                returnValue = action.params.content;
+                break;
+              }
             }
           } else {
             let originalUrl = ReaderMode.getOriginalUrl(aValue);
             if (originalUrl) {
               returnValue = originalUrl;
             }
           }
 
@@ -473,16 +482,23 @@ file, You can obtain one at http://mozil
                   action.params.engineName,
                   action.params.searchSuggestion || action.params.searchQuery,
                   event,
                   where,
                   openUILinkParams,
                   actionDetails
                 );
                 break;
+              case "extension":
+                this.handleRevert();
+                // Give the extension control of handling the command.
+                let searchString = action.params.content;
+                let keyword = action.params.keyword;
+                this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
+                return;
             }
           } else {
             // This is a fallback for add-ons and old testing code that directly
             // set value and try to confirm it. UnifiedComplete should always
             // resolve to a valid url.
             try {
               new URL(url);
             } catch (ex) {
@@ -590,17 +606,17 @@ file, You can obtain one at http://mozil
         <parameter name="searchActionDetails"/>
         <body><![CDATA[
           let engine =
             typeof(engineOrEngineName) == "string" ?
               Services.search.getEngineByName(engineOrEngineName) :
               engineOrEngineName;
           let isOneOff = this.popup.oneOffSearchButtons
               .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
-          // Infer the type of the even which triggered the search.
+          // Infer the type of the event which triggered the search.
           let eventType = "unknown";
           if (event instanceof KeyboardEvent) {
             eventType = "key";
           } else if (event instanceof MouseEvent) {
             eventType = "mouse";
           }
           // Augment the search action details object.
           let details = searchActionDetails || {};
@@ -1168,16 +1184,19 @@ file, You can obtain one at http://mozil
         }
       ]]></handler>
 
       <handler event="blur"><![CDATA[
         if (event.originalTarget == this.inputField) {
           this._clearNoActions();
           this.formatValue();
         }
+        if (ExtensionSearchHandler.hasActiveInputSession()) {
+          ExtensionSearchHandler.handleInputCancelled();
+        }
       ]]></handler>
 
       <handler event="dragstart" phase="capturing"><![CDATA[
         // Drag only if the gesture starts from the input field.
         if (this.inputField != event.originalTarget &&
             !(this.inputField.compareDocumentPosition(event.originalTarget) &
               Node.DOCUMENT_POSITION_CONTAINED_BY))
           return;
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-c-omnibox.js
@@ -0,0 +1,32 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+  runSafeSyncWithoutClone,
+  SingletonEventManager,
+} = ExtensionUtils;
+
+extensions.registerSchemaAPI("omnibox", "addon_child", context => {
+  return {
+    omnibox: {
+      onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => {
+        let listener = (text, id) => {
+          runSafeSyncWithoutClone(fire, text, suggestions => {
+            // TODO: Switch to using callParentFunctionNoReturn once bug 1314903 is fixed.
+            context.childManager.callParentAsyncFunction("omnibox_internal.addSuggestions", [
+              id,
+              suggestions,
+            ]);
+          });
+        };
+        context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
+        return () => {
+          context.childManager.getParentEvent("omnibox_internal.onInputChanged").removeListener(listener);
+        };
+      }).api(),
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-omnibox.js
@@ -0,0 +1,104 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
+                                  "resource://gre/modules/ExtensionSearchHandler.jsm");
+var {
+  SingletonEventManager,
+} = ExtensionUtils;
+
+// WeakMap[extension -> keyword]
+let gKeywordMap = new WeakMap();
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_omnibox", (type, directive, extension, manifest) => {
+  let keyword = manifest.omnibox.keyword;
+  try {
+    // This will throw if the keyword is already registered.
+    ExtensionSearchHandler.registerKeyword(keyword, extension);
+    gKeywordMap.set(extension, keyword);
+  } catch (e) {
+    extension.manifestError(e.message);
+  }
+});
+
+extensions.on("shutdown", (type, extension) => {
+  let keyword = gKeywordMap.get(extension);
+  if (keyword) {
+    ExtensionSearchHandler.unregisterKeyword(keyword);
+    gKeywordMap.delete(extension);
+  }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("omnibox", "addon_parent", context => {
+  let {extension} = context;
+  return {
+    omnibox: {
+      setDefaultSuggestion(suggestion) {
+        let keyword = gKeywordMap.get(extension);
+        try {
+          // This will throw if the keyword failed to register.
+          ExtensionSearchHandler.setDefaultSuggestion(keyword, suggestion);
+        } catch (e) {
+          return Promise.reject(e.message);
+        }
+      },
+
+      onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => {
+        let listener = (eventName) => {
+          fire();
+        };
+        extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+        return () => {
+          extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+        };
+      }).api(),
+
+      onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => {
+        let listener = (eventName) => {
+          fire();
+        };
+        extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+        return () => {
+          extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+        };
+      }).api(),
+
+      onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => {
+        let listener = (eventName, text, disposition) => {
+          fire(text, disposition);
+        };
+        extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+        return () => {
+          extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+        };
+      }).api(),
+    },
+
+    omnibox_internal: {
+      addSuggestions(id, suggestions) {
+        let keyword = gKeywordMap.get(extension);
+        try {
+          ExtensionSearchHandler.addSuggestions(keyword, id, suggestions);
+        } catch (e) {
+          // Silently fail because the extension developer can not know for sure if the user
+          // has already invalidated the callback when asynchronously providing suggestions.
+        }
+      },
+
+      onInputChanged: new SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => {
+        let listener = (eventName, text, id) => {
+          fire(text, id);
+        };
+        extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+        return () => {
+          extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+        };
+      }).api(),
+    },
+  };
+});
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,28 +1,31 @@
 # scripts
 category webextension-scripts bookmarks chrome://browser/content/ext-bookmarks.js
 category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
 category webextension-scripts commands chrome://browser/content/ext-commands.js
 category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
 category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
 category webextension-scripts history chrome://browser/content/ext-history.js
+category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
 category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
 category webextension-scripts sessions chrome://browser/content/ext-sessions.js
 category webextension-scripts tabs chrome://browser/content/ext-tabs.js
 category webextension-scripts utils chrome://browser/content/ext-utils.js
 category webextension-scripts windows chrome://browser/content/ext-windows.js
 
 # scripts that must run in the same process as addon code.
 category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
+category webextension-scripts-addon omnibox chrome://browser/content/ext-c-omnibox.js
 category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
 
 # schemas
 category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
 category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
 category webextension-schemas commands chrome://browser/content/schemas/commands.json
 category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
 category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
 category webextension-schemas history chrome://browser/content/schemas/history.json
+category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
 category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
 category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
 category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
 category webextension-schemas windows chrome://browser/content/schemas/windows.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -13,15 +13,17 @@ browser.jar:
 #endif
     content/browser/extension.svg
     content/browser/ext-bookmarks.js
     content/browser/ext-browserAction.js
     content/browser/ext-commands.js
     content/browser/ext-contextMenus.js
     content/browser/ext-desktop-runtime.js
     content/browser/ext-history.js
+    content/browser/ext-omnibox.js
     content/browser/ext-pageAction.js
     content/browser/ext-sessions.js
     content/browser/ext-tabs.js
     content/browser/ext-utils.js
     content/browser/ext-windows.js
     content/browser/ext-c-contextMenus.js
+    content/browser/ext-c-omnibox.js
     content/browser/ext-c-tabs.js
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -4,12 +4,13 @@
 
 browser.jar:
     content/browser/schemas/bookmarks.json
     content/browser/schemas/browser_action.json
     content/browser/schemas/commands.json
     content/browser/schemas/context_menus.json
     content/browser/schemas/context_menus_internal.json
     content/browser/schemas/history.json
+    content/browser/schemas/omnibox.json
     content/browser/schemas/page_action.json
     content/browser/schemas/sessions.json
     content/browser/schemas/tabs.json
     content/browser/schemas/windows.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/omnibox.json
@@ -0,0 +1,248 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "omnibox": {
+            "type": "object",
+            "additionalProperties": { "$ref": "UnrecognizedProperty" },
+            "properties": {
+              "keyword": {
+                "type": "string",
+                "pattern": "^[^?\\s:]([^\\s:]*[^/\\s:])?$"
+              }
+            },
+            "optional": true
+          }
+        }
+      }
+    ]
+  },
+  {
+    "namespace": "omnibox",
+    "description": "The omnibox API allows you to register a keyword with Firefox's address bar.",
+    "permissions": ["manifest:omnibox"],
+    "types": [
+      {
+        "id": "DescriptionStyleType",
+        "type": "string",
+        "description": "The style type.",
+        "enum": ["url", "match", "dim"]
+      },
+      {
+        "id": "OnInputEnteredDisposition",
+        "type": "string",
+        "enum": ["currentTab", "newForegroundTab", "newBackgroundTab"],
+        "description": "The window disposition for the omnibox query. This is the recommended context to display results. For example, if the omnibox command is to navigate to a certain URL, a disposition of 'newForegroundTab' means the navigation should take place in a new selected tab."
+      },
+      {
+        "id": "SuggestResult",
+        "type": "object",
+        "description": "A suggest result.",
+        "properties": {
+          "content": {
+            "type": "string",
+            "minLength": 1,
+            "description": "The text that is put into the URL bar, and that is sent to the extension when the user chooses this entry."
+          },
+          "description": {
+            "type": "string",
+            "minLength": 1,
+            "description": "The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. <dim><match>dimmed match</match></dim>. You must escape the five predefined entities to display them as text: stackoverflow.com/a/1091953/89484 "
+          },
+          "descriptionStyles": {
+            "optional": true,
+            "unsupported": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by the extension.",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by the extension.",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "description": "The style type", "$ref": "DescriptionStyleType"},
+                "length": { "type": "integer", "optional": true }
+              }
+            }
+          },
+          "descriptionStylesRaw": {
+            "optional": true,
+            "unsupported": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by ToValue().",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by ToValue().",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "type": "integer" }
+              }
+            }
+          }
+        }
+      },
+      {
+        "id": "DefaultSuggestResult",
+        "type": "object",
+        "description": "A suggest result.",
+        "properties": {
+          "description": {
+            "type": "string",
+            "minLength": 1,
+            "description": "The text that is displayed in the URL dropdown."
+          },
+          "descriptionStyles": {
+            "optional": true,
+            "unsupported": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by the extension.",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by the extension.",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "description": "The style type", "$ref": "DescriptionStyleType"},
+                "length": { "type": "integer", "optional": true }
+              }
+            }
+          },
+          "descriptionStylesRaw": {
+            "optional": true,
+            "unsupported": true,
+            "type": "array",
+            "description": "An array of style ranges for the description, as provided by ToValue().",
+            "items": {
+              "type": "object",
+              "description": "The style ranges for the description, as provided by ToValue().",
+              "properties": {
+                "offset": { "type": "integer" },
+                "type": { "type": "integer" }
+              }
+            }
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "setDefaultSuggestion",
+        "type": "function",
+        "description": "Sets the description and styling for the default suggestion. The default suggestion is the text that is displayed in the first suggestion row underneath the URL bar.",
+        "parameters": [
+          {
+            "name": "suggestion",
+            "$ref": "DefaultSuggestResult",
+            "description": "A partial SuggestResult object, without the 'content' parameter."
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onInputStarted",
+        "type": "function",
+        "description": "User has started a keyword input session by typing the extension's keyword. This is guaranteed to be sent exactly once per input session, and before any onInputChanged events.",
+        "parameters": []
+      },
+      {
+        "name": "onInputChanged",
+        "type": "function",
+        "description": "User has changed what is typed into the omnibox.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "text"
+          },
+          {
+            "name": "suggest",
+            "type": "function",
+            "description": "A callback passed to the onInputChanged event used for sending suggestions back to the browser.",
+            "parameters": [
+              {
+                "name": "suggestResults",
+                "type": "array",
+                "description": "Array of suggest results",
+                "items": {
+                  "$ref": "SuggestResult"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "onInputEntered",
+        "type": "function",
+        "description": "User has accepted what is typed into the omnibox.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "text"
+          },
+          {
+            "name": "disposition",
+            "$ref": "OnInputEnteredDisposition"
+          }
+        ]
+      },
+      {
+        "name": "onInputCancelled",
+        "type": "function",
+        "description": "User has ended the keyword input session without accepting the input.",
+        "parameters": []
+      }
+    ]
+  },
+  {
+    "namespace": "omnibox_internal",
+    "description": "The internal namespace used by the omnibox API.",
+    "defaultContexts": ["addon_parent_only"],
+    "functions": [
+      {
+        "name": "addSuggestions",
+        "type": "function",
+        "async": "callback",
+        "description": "Internal function used by omnibox.onInputChanged for adding search suggestions",
+        "parameters": [
+          {
+            "name": "id",
+            "type": "integer",
+            "description": "The ID of the callback received by onInputChangedInternal"
+          },
+          {
+            "name": "suggestResults",
+            "type": "array",
+            "description": "Array of suggest results",
+            "items": {
+              "$ref": "omnibox.SuggestResult"
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onInputChanged",
+        "type": "function",
+        "description": "Identical to omnibox.onInputChanged except no 'suggest' callback is provided.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "text"
+          }
+        ]
+      }
+    ]
+  }
+]
\ No newline at end of file
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -40,16 +40,17 @@ tags = webextensions
 [browser_ext_contextMenus_radioGroups.js]
 [browser_ext_contextMenus_uninstall.js]
 [browser_ext_contextMenus_urlPatterns.js]
 [browser_ext_currentWindow.js]
 [browser_ext_getViews.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
 [browser_ext_legacy_extension_context_contentscript.js]
+[browser_ext_omnibox.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_pageAction_popup_resize.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_pageAction_title.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_popup_background.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js
@@ -0,0 +1,286 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function* setup() {
+  const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+  });
+}
+
+add_task(function* () {
+  let keyword = "test";
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "omnibox": {
+        "keyword": keyword,
+      },
+    },
+
+    background: function() {
+      browser.omnibox.onInputStarted.addListener(() => {
+        browser.test.sendMessage("on-input-started-fired");
+      });
+
+      let synchronous = true;
+      let suggestions = null;
+      let suggestCallback = null;
+
+      browser.omnibox.onInputChanged.addListener((text, suggest) => {
+        if (synchronous && suggestions) {
+          suggest(suggestions);
+        } else {
+          suggestCallback = suggest;
+        }
+        browser.test.sendMessage("on-input-changed-fired", {text});
+      });
+
+      browser.omnibox.onInputCancelled.addListener(() => {
+        browser.test.sendMessage("on-input-cancelled-fired");
+      });
+
+      browser.omnibox.onInputEntered.addListener((text, disposition) => {
+        browser.test.sendMessage("on-input-entered-fired", {text, disposition});
+      });
+
+      browser.test.onMessage.addListener((msg, data) => {
+        switch (msg) {
+          case "set-suggestions":
+            suggestions = data.suggestions;
+            browser.test.sendMessage("suggestions-set");
+            break;
+          case "set-default-suggestion":
+            browser.omnibox.setDefaultSuggestion(data.suggestion);
+            browser.test.sendMessage("default-suggestion-set");
+            break;
+          case "set-synchronous":
+            synchronous = data.synchronous;
+            break;
+          case "test-multiple-suggest-calls":
+            suggestions.forEach(suggestion => suggestCallback([suggestion]));
+            browser.test.sendMessage("test-ready");
+            break;
+          case "test-suggestions-after-delay":
+            Promise.resolve().then(() => {
+              suggestCallback(suggestions);
+              browser.test.sendMessage("test-ready");
+            });
+            break;
+        }
+      });
+    },
+  });
+
+  function* expectEvent(event, expected = {}) {
+    let actual = yield extension.awaitMessage(event);
+    if (expected.text) {
+      is(actual.text, expected.text,
+        `Expected "${event}" to have fired with text: "${expected.text}".`);
+    }
+    if (expected.disposition) {
+      is(actual.disposition, expected.disposition,
+        `Expected "${event}" to have fired with disposition: "${expected.disposition}".`);
+    }
+  }
+
+  function* startInputSession() {
+    gURLBar.focus();
+    gURLBar.value = keyword;
+    EventUtils.synthesizeKey(" ", {});
+    yield expectEvent("on-input-started-fired");
+    EventUtils.synthesizeKey("t", {});
+    yield expectEvent("on-input-changed-fired", {text: "t"});
+    return "t";
+  }
+
+  function* testInputEvents() {
+    gURLBar.focus();
+
+    // Start an input session by typing in <keyword><space>.
+    for (let letter of keyword) {
+      EventUtils.synthesizeKey(letter, {});
+    }
+    EventUtils.synthesizeKey(" ", {});
+    yield expectEvent("on-input-started-fired");
+
+    // We should expect input changed events now that the keyword is active.
+    EventUtils.synthesizeKey("b", {});
+    yield expectEvent("on-input-changed-fired", {text: "b"});
+
+    EventUtils.synthesizeKey("c", {});
+    yield expectEvent("on-input-changed-fired", {text: "bc"});
+
+    EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+    yield expectEvent("on-input-changed-fired", {text: "b"});
+
+    // Even though the input is <keyword><space> We should not expect an
+    // input started event to fire since the keyword is active.
+    EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+    yield expectEvent("on-input-changed-fired", {text: ""});
+
+    // Make the keyword inactive by hitting backspace.
+    EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+    yield expectEvent("on-input-cancelled-fired");
+
+    // Activate the keyword by typing a space.
+    // Expect onInputStarted to fire.
+    EventUtils.synthesizeKey(" ", {});
+    yield expectEvent("on-input-started-fired");
+
+    // onInputChanged should fire even if a space is entered.
+    EventUtils.synthesizeKey(" ", {});
+    yield expectEvent("on-input-changed-fired", {text: " "});
+
+    // The active session should cancel if the input blurs.
+    gURLBar.blur();
+    yield expectEvent("on-input-cancelled-fired");
+  }
+
+  function* testHeuristicResult(expectedText, setDefaultSuggestion) {
+    if (setDefaultSuggestion) {
+      extension.sendMessage("set-default-suggestion", {
+        suggestion: {
+          description: expectedText,
+        },
+      });
+      yield extension.awaitMessage("default-suggestion-set");
+    }
+
+    let text = yield startInputSession();
+
+    let item = gURLBar.popup.richlistbox.children[0];
+
+    is(item.getAttribute("title"), expectedText,
+      `Expected heuristic result to have title: "${expectedText}".`);
+
+    is(item.getAttribute("displayurl"), `${keyword} ${text}`,
+      `Expected heuristic result to have displayurl: "${keyword} ${text}".`);
+
+    EventUtils.synthesizeMouseAtCenter(item, {});
+
+    yield expectEvent("on-input-entered-fired", {
+      text,
+      disposition: "currentTab",
+    });
+  }
+
+  function* testDisposition(suggestionIndex, expectedDisposition, expectedText) {
+    yield startInputSession();
+
+    // Select the suggestion.
+    for (let i = 0; i < suggestionIndex; i++) {
+      EventUtils.synthesizeKey("VK_DOWN", {});
+    }
+
+    let item = gURLBar.popup.richlistbox.children[suggestionIndex];
+    if (expectedDisposition == "currentTab") {
+      EventUtils.synthesizeMouseAtCenter(item, {});
+    } else if (expectedDisposition == "newForegroundTab") {
+      EventUtils.synthesizeMouseAtCenter(item, {accelKey: true});
+    } else if (expectedDisposition == "newBackgroundTab") {
+      EventUtils.synthesizeMouseAtCenter(item, {shiftKey: true, accelKey: true});
+    }
+
+    yield expectEvent("on-input-entered-fired", {
+      text: expectedText,
+      disposition: expectedDisposition,
+    });
+  }
+
+  function* testSuggestions(info) {
+    extension.sendMessage("set-synchronous", {synchronous: false});
+
+    function expectSuggestion({content, description}, index) {
+      let item = gURLBar.popup.richlistbox.children[index + 1]; // Skip the heuristic result.
+
+      ok(!!item, "Expected item to exist");
+      is(item.getAttribute("title"), description,
+        `Expected suggestion to have title: "${description}".`);
+
+      is(item.getAttribute("displayurl"), `${keyword} ${content}`,
+        `Expected suggestion to have displayurl: "${keyword} ${content}".`);
+    }
+
+    let text = yield startInputSession();
+
+    extension.sendMessage(info.test);
+    yield extension.awaitMessage("test-ready");
+
+    info.suggestions.forEach(expectSuggestion);
+
+    EventUtils.synthesizeMouseAtCenter(gURLBar.popup.richlistbox.children[0], {});
+    yield expectEvent("on-input-entered-fired", {
+      text,
+      disposition: "currentTab",
+    });
+  }
+
+  yield setup();
+  yield extension.startup();
+
+  yield testInputEvents();
+
+  // Test the heuristic result with default suggestions.
+  yield testHeuristicResult("Generated extension", false /* setDefaultSuggestion */);
+  yield testHeuristicResult("hello world", true /* setDefaultSuggestion */);
+  yield testHeuristicResult("foo bar", true /* setDefaultSuggestion */);
+
+  let suggestions = [
+    {content: "a", description: "select a"},
+    {content: "b", description: "select b"},
+    {content: "c", description: "select c"},
+  ];
+
+  extension.sendMessage("set-suggestions", {suggestions});
+  yield extension.awaitMessage("suggestions-set");
+
+  // Test each suggestion and search disposition.
+  yield testDisposition(1, "currentTab", suggestions[0].content);
+  yield testDisposition(2, "newForegroundTab", suggestions[1].content);
+  yield testDisposition(3, "newBackgroundTab", suggestions[2].content);
+
+  extension.sendMessage("set-suggestions", {suggestions});
+  yield extension.awaitMessage("suggestions-set");
+
+  // Test adding suggestions asynchronously.
+  yield testSuggestions({
+    test: "test-multiple-suggest-calls",
+    skipHeuristic: true,
+    suggestions,
+  });
+  yield testSuggestions({
+    test: "test-suggestions-after-delay",
+    skipHeuristic: true,
+    suggestions,
+  });
+
+  // Start monitoring the console.
+  SimpleTest.waitForExplicitFinish();
+  let waitForConsole = new Promise(resolve => {
+    SimpleTest.monitorConsole(resolve, [{
+      message: new RegExp(`The keyword provided is already registered: "${keyword}"`),
+    }]);
+  });
+
+  // Try registering another extension with the same keyword
+  let extension2 = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "omnibox": {
+        "keyword": keyword,
+      },
+    },
+  });
+
+  yield extension2.startup();
+
+  // Stop monitoring the console and confirm the correct errors are logged.
+  SimpleTest.endMonitorConsole();
+  yield waitForConsole;
+
+  yield extension2.unload();
+  yield extension.unload();
+});
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
@@ -5,16 +5,27 @@
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
                                   "resource://testing-common/PlacesTestUtils.jsm");
 
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
+function* promiseAutocompleteResultPopup(inputText) {
+  gURLBar.focus();
+  gURLBar.value = inputText;
+  gURLBar.controller.startSearch(inputText);
+  yield promisePopupShown(gURLBar.popup);
+  yield BrowserTestUtils.waitForCondition(() => {
+    return gURLBar.controller.searchStatus >=
+      Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+  });
+}
+
 function* addBookmark(bookmark) {
   if (bookmark.keyword) {
     yield PlacesUtils.keywords.insert({
       keyword: bookmark.keyword,
       url: bookmark.url,
     });
   }
 
@@ -137,27 +148,19 @@ add_task(function* test_webnavigation_ur
     title: "Bookmark To Click",
     url: "http://example.com/?q=bookmark",
   });
 
   yield extension.startup();
 
   yield extension.awaitMessage("ready");
 
-  gURLBar.focus();
-  gURLBar.value = "Bookmark To Click";
-  gURLBar.controller.startSearch("Bookmark To Click");
-
-  let item;
+  yield promiseAutocompleteResultPopup("Bookmark To Click");
 
-  yield BrowserTestUtils.waitForCondition(() => {
-    item = gURLBar.popup.richlistbox.getItemAtIndex(1);
-    return item;
-  });
-
+  let item = gURLBar.popup.richlistbox.getItemAtIndex(1);
   item.click();
   yield extension.awaitFinish("webNavigation.from_address_bar.auto_bookmark");
 
   yield extension.unload();
   info("extension unloaded");
 });
 
 add_task(function* test_webnavigation_urlbar_keyword_transition() {
@@ -190,23 +193,17 @@ add_task(function* test_webnavigation_ur
     url: "http://example.com/?q=%s",
     keyword: "testkw",
   });
 
   yield extension.startup();
 
   yield extension.awaitMessage("ready");
 
-  gURLBar.focus();
-  gURLBar.value = "testkw search";
-  gURLBar.controller.startSearch("testkw search");
-
-  yield BrowserTestUtils.waitForCondition(() => {
-    return gURLBar.popup.input.controller.matchCount;
-  });
+  yield promiseAutocompleteResultPopup("testkw search");
 
   let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
   item.click();
 
   yield extension.awaitFinish("webNavigation.from_address_bar.keyword");
 
   yield extension.unload();
   info("extension unloaded");
@@ -237,24 +234,17 @@ add_task(function* test_webnavigation_ur
     },
   });
 
   yield extension.startup();
 
   yield extension.awaitMessage("ready");
 
   yield prepareSearchEngine();
-
-  gURLBar.focus();
-  gURLBar.value = "foo";
-  gURLBar.controller.startSearch("foo");
-
-  yield BrowserTestUtils.waitForCondition(() => {
-    return gURLBar.popup.input.controller.matchCount;
-  });
+  yield promiseAutocompleteResultPopup("foo");
 
   let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
   item.click();
 
   yield extension.awaitFinish("webNavigation.from_address_bar.generated");
 
   yield extension.unload();
   info("extension unloaded");
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js
@@ -0,0 +1,61 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function* testKeyword(params) {
+  let normalized = yield ExtensionTestUtils.normalizeManifest({
+    "omnibox": {
+      "keyword": params.keyword,
+    },
+  });
+
+  if (params.expectError) {
+    let expectedError = (
+      String.raw`omnibox.keyword: String "${params.keyword}" ` +
+      String.raw`must match /^[^?\s:]([^\s:]*[^/\s:])?$/`
+    );
+    ok(normalized.error.includes(expectedError),
+       `The manifest error ${JSON.stringify(normalized.error)} ` +
+       `must contain ${JSON.stringify(expectedError)}`);
+  } else {
+    equal(normalized.error, undefined, "Should not have an error");
+    equal(normalized.errors.length, 0, "Should not have warnings");
+  }
+}
+
+add_task(function* test_manifest_commands() {
+  // accepted single character keywords
+  yield testKeyword({keyword: "a", expectError: false});
+  yield testKeyword({keyword: "-", expectError: false});
+  yield testKeyword({keyword: "嗨", expectError: false});
+  yield testKeyword({keyword: "*", expectError: false});
+  yield testKeyword({keyword: "/", expectError: false});
+
+  // rejected single character keywords
+  yield testKeyword({keyword: "?", expectError: true});
+  yield testKeyword({keyword: " ", expectError: true});
+  yield testKeyword({keyword: ":", expectError: true});
+
+  // accepted multi-character keywords
+  yield testKeyword({keyword: "aa", expectError: false});
+  yield testKeyword({keyword: "http", expectError: false});
+  yield testKeyword({keyword: "f?a", expectError: false});
+  yield testKeyword({keyword: "fa?", expectError: false});
+  yield testKeyword({keyword: "f/x", expectError: false});
+  yield testKeyword({keyword: "/fx", expectError: false});
+
+  // rejected multi-character keywords
+  yield testKeyword({keyword: " a", expectError: true});
+  yield testKeyword({keyword: "a ", expectError: true});
+  yield testKeyword({keyword: "  ", expectError: true});
+  yield testKeyword({keyword: " a ", expectError: true});
+  yield testKeyword({keyword: "?fx", expectError: true});
+  yield testKeyword({keyword: "fx/", expectError: true});
+  yield testKeyword({keyword: "f:x", expectError: true});
+  yield testKeyword({keyword: "fx:", expectError: true});
+  yield testKeyword({keyword: "f x", expectError: true});
+
+  // miscellaneous tests
+  yield testKeyword({keyword: "こんにちは", expectError: false});
+  yield testKeyword({keyword: "http://", expectError: true});
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell.ini
@@ -2,9 +2,10 @@
 head = head.js
 tail =
 firefox-appdir = browser
 tags = webextensions
 
 [test_ext_bookmarks.js]
 [test_ext_history.js]
 [test_ext_manifest_commands.js]
+[test_ext_manifest_omnibox.js]
 [test_ext_manifest_permissions.js]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -416,16 +416,17 @@ BrowserGlue.prototype = {
       history: 2,
       keyword: 3,
       searchengine: 4,
       searchsuggestion: 5,
       switchtab: 6,
       tag: 7,
       visiturl: 8,
       remotetab: 9,
+      extension: 10,
     };
     if (actionType in buckets) {
       Services.telemetry
               .getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE")
               .add(buckets[actionType]);
     } else {
       Cu.reportError("Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " +
                      actionType);
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -387,16 +387,17 @@ These should match what Safari and other
 <!ENTITY customizeMenu.removeFromMenu.label "Remove from Menu">
 <!ENTITY customizeMenu.removeFromMenu.accesskey "R">
 <!ENTITY customizeMenu.addMoreItems.label "Add More Items…">
 <!ENTITY customizeMenu.addMoreItems.accesskey "A">
 
 <!ENTITY openCmd.commandkey           "l">
 <!ENTITY urlbar.placeholder2          "Search or enter address">
 <!ENTITY urlbar.accesskey             "d">
+<!ENTITY urlbar.extension.label       "Extension:">
 <!ENTITY urlbar.switchToTab.label     "Switch to tab:">
 
 <!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">
 <!ENTITY urlbar.searchSuggestionsNotification.learnMore "Learn more…">
 <!ENTITY urlbar.searchSuggestionsNotification.learnMore.accesskey "l">
 <!ENTITY urlbar.searchSuggestionsNotification.disable "No">
 <!ENTITY urlbar.searchSuggestionsNotification.disable.accesskey "n">
 <!ENTITY urlbar.searchSuggestionsNotification.enable "Yes">
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -74,16 +74,23 @@
 
 #urlbar[actiontype="searchengine"] > #identity-box > #identity-icon {
   -moz-image-region: inherit;
   list-style-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
   width: 16px;
   height: 16px;
 }
 
+#urlbar[actiontype="extension"] > #identity-box > #identity-icon {
+  -moz-image-region: inherit;
+  list-style-image: url(chrome://browser/skin/addons/addon-install-anchor.svg);
+  width: 16px;
+  height: 16px;
+}
+
 /* SHARING ICON */
 
 #sharing-icon {
   width: 16px;
   height: 16px;
   margin-inline-start: -16px;
   position: relative;
   display: none;
--- a/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
+++ b/devtools/client/animationinspector/test/browser_animation_animated_properties_displayed.js
@@ -6,19 +6,24 @@
 
 const LAYOUT_ERRORS_L10N =
   new LocalizationHelper("toolkit/locales/layout_errors.properties");
 
 // Test that when an animation is selected, its list of animated properties is
 // displayed below it.
 
 const EXPECTED_PROPERTIES = [
+  "background-attachment",
+  "background-clip",
   "background-color",
+  "background-image",
+  "background-origin",
   "background-position-x",
   "background-position-y",
+  "background-repeat",
   "background-size",
   "border-bottom-left-radius",
   "border-bottom-right-radius",
   "border-top-left-radius",
   "border-top-right-radius",
   "filter",
   "height",
   "transform",
--- a/dom/animation/test/chrome/test_animation_properties.html
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -172,17 +172,57 @@ var gTests = [
                 { property: 'border-left-width',
                   values: [ value(0, '1px', 'replace', 'linear'),
                             value(1, '2px', 'replace') ] },
                 { property: 'border-right-width',
                   values: [ value(0, '3px', 'replace', 'linear'),
                             value(1, '4px', 'replace') ] },
                 { property: 'border-top-width',
                   values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] } ]
+                            value(1, '4px', 'replace') ] },
+                { property: 'border-bottom-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-left-style',
+                  values: [ value(0, 'solid', 'replace', 'linear'),
+                            value(1, 'solid', 'replace') ] },
+                { property: 'border-right-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-top-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-image-outset',
+                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+                            value(1, '0 0 0 0', 'replace') ] },
+                { property: 'border-image-repeat',
+                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+                            value(1, 'stretch stretch', 'replace') ] },
+                { property: 'border-image-slice',
+                  values: [ value(0, '100% 100% 100% 100%',
+                                  'replace', 'linear'),
+                            value(1, '100% 100% 100% 100%', 'replace') ] },
+                { property: 'border-image-source',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: 'border-image-width',
+                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+                            value(1, '1 1 1 1', 'replace') ] },
+                { property: '-moz-border-bottom-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-left-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-right-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-top-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] } ]
   },
   { desc:     'a property-indexed keyframe where a greater shorthand precedes'
               + ' a lesser shorthand',
     frames:   { border:     [ '3px dotted rgb(7, 8, 9)',
                               '4px dashed rgb(10, 11, 12)' ],
                 borderLeft: [ '1px solid rgb(1, 2, 3)',
                               '2px solid rgb(4, 5, 6)' ] },
     expected: [ { property: 'border-bottom-color',
@@ -203,17 +243,57 @@ var gTests = [
                 { property: 'border-left-width',
                   values: [ value(0, '1px', 'replace', 'linear'),
                             value(1, '2px', 'replace') ] },
                 { property: 'border-right-width',
                   values: [ value(0, '3px', 'replace', 'linear'),
                             value(1, '4px', 'replace') ] },
                 { property: 'border-top-width',
                   values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] } ]
+                            value(1, '4px', 'replace') ] },
+                { property: 'border-bottom-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-left-style',
+                  values: [ value(0, 'solid', 'replace', 'linear'),
+                            value(1, 'solid', 'replace') ] },
+                { property: 'border-right-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-top-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-image-outset',
+                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+                            value(1, '0 0 0 0', 'replace') ] },
+                { property: 'border-image-repeat',
+                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+                            value(1, 'stretch stretch', 'replace') ] },
+                { property: 'border-image-slice',
+                  values: [ value(0, '100% 100% 100% 100%',
+                                  'replace', 'linear'),
+                            value(1, '100% 100% 100% 100%', 'replace') ] },
+                { property: 'border-image-source',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: 'border-image-width',
+                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+                            value(1, '1 1 1 1', 'replace') ] },
+                { property: '-moz-border-bottom-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-left-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-right-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-top-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] } ]
   },
 
   // ---------------------------------------------------------------------
   //
   // Tests for keyframe sequences
   //
   // ---------------------------------------------------------------------
 
@@ -443,17 +523,57 @@ var gTests = [
                 { property: 'border-left-width',
                   values: [ value(0, '1px', 'replace', 'linear'),
                             value(1, '3px', 'replace') ] },
                 { property: 'border-right-width',
                   values: [ value(0, '2px', 'replace', 'linear'),
                             value(1, '3px', 'replace') ] },
                 { property: 'border-top-width',
                   values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] } ]
+                            value(1, '3px', 'replace') ] },
+                { property: 'border-bottom-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-left-style',
+                  values: [ value(0, 'solid', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-right-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-top-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-image-outset',
+                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+                            value(1, '0 0 0 0', 'replace') ] },
+                { property: 'border-image-repeat',
+                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+                            value(1, 'stretch stretch', 'replace') ] },
+                { property: 'border-image-slice',
+                  values: [ value(0, '100% 100% 100% 100%',
+                                  'replace', 'linear'),
+                            value(1, '100% 100% 100% 100%', 'replace') ] },
+                { property: 'border-image-source',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: 'border-image-width',
+                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+                            value(1, '1 1 1 1', 'replace') ] },
+                { property: '-moz-border-bottom-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-left-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-right-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-top-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] } ]
   },
   { desc:     'a keyframe sequence where greater shorthand precedes'
               + ' lesser shorthand',
     frames:   [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
                              borderLeft: '1px solid rgb(1, 2, 3)' },
                 { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
     expected: [ { property: 'border-bottom-color',
                   values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
@@ -473,17 +593,57 @@ var gTests = [
                 { property: 'border-left-width',
                   values: [ value(0, '1px', 'replace', 'linear'),
                             value(1, '3px', 'replace') ] },
                 { property: 'border-right-width',
                   values: [ value(0, '2px', 'replace', 'linear'),
                             value(1, '3px', 'replace') ] },
                 { property: 'border-top-width',
                   values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] } ]
+                            value(1, '3px', 'replace') ] },
+                { property: 'border-bottom-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-left-style',
+                  values: [ value(0, 'solid', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-right-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-top-style',
+                  values: [ value(0, 'dotted', 'replace', 'linear'),
+                            value(1, 'dashed', 'replace') ] },
+                { property: 'border-image-outset',
+                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
+                            value(1, '0 0 0 0', 'replace') ] },
+                { property: 'border-image-repeat',
+                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
+                            value(1, 'stretch stretch', 'replace') ] },
+                { property: 'border-image-slice',
+                  values: [ value(0, '100% 100% 100% 100%',
+                                  'replace', 'linear'),
+                            value(1, '100% 100% 100% 100%', 'replace') ] },
+                { property: 'border-image-source',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: 'border-image-width',
+                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
+                            value(1, '1 1 1 1', 'replace') ] },
+                { property: '-moz-border-bottom-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-left-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-right-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] },
+                { property: '-moz-border-top-colors',
+                  values: [ value(0, 'none', 'replace', 'linear'),
+                            value(1, 'none', 'replace') ] } ]
   },
 
   // ---------------------------------------------------------------------
   //
   // Tests for unit conversion
   //
   // ---------------------------------------------------------------------
 
--- a/dom/animation/test/mochitest.ini
+++ b/dom/animation/test/mochitest.ini
@@ -36,16 +36,17 @@ support-files =
   css-transitions/file_keyframeeffect-getkeyframes.html
   css-transitions/file_pseudoElement-get-animations.html
   css-transitions/file_setting-effect.html
   document-timeline/file_document-timeline.html
   mozilla/file_cubic_bezier_limits.html
   mozilla/file_deferred_start.html
   mozilla/file_disabled_properties.html
   mozilla/file_disable_animations_api_core.html
+  mozilla/file_discrete-animations.html
   mozilla/file_document-timeline-origin-time-range.html
   mozilla/file_hide_and_show.html
   mozilla/file_partial_keyframes.html
   mozilla/file_spacing_property_order.html
   mozilla/file_spacing_transform.html
   mozilla/file_transform_limits.html
   mozilla/file_transition_finish_on_compositor.html
   mozilla/file_underlying-discrete-value.html
@@ -90,16 +91,17 @@ support-files =
 [css-transitions/test_pseudoElement-get-animations.html]
 [css-transitions/test_setting-effect.html]
 [document-timeline/test_document-timeline.html]
 [document-timeline/test_request_animation_frame.html]
 [mozilla/test_cubic_bezier_limits.html]
 [mozilla/test_deferred_start.html]
 [mozilla/test_disable_animations_api_core.html]
 [mozilla/test_disabled_properties.html]
+[mozilla/test_discrete-animations.html]
 [mozilla/test_document-timeline-origin-time-range.html]
 [mozilla/test_hide_and_show.html]
 [mozilla/test_partial_keyframes.html]
 [mozilla/test_set-easing.html]
 [mozilla/test_spacing_property_order.html]
 [mozilla/test_spacing_transform.html]
 [mozilla/test_transform_limits.html]
 [mozilla/test_transition_finish_on_compositor.html]
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/mozilla/file_discrete-animations.html
@@ -0,0 +1,170 @@
+<!doctype html>
+<head>
+<meta charset=utf-8>
+<title>Test Mozilla-specific discrete animatable properties</title>
+<script type="application/javascript" src="../testcommon.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+const gMozillaSpecificProperties = {
+  "-moz-appearance": {
+    // https://drafts.csswg.org/css-align/#propdef-align-content
+    from: "button",
+    to: "none"
+  },
+  "-moz-border-bottom-colors": {
+    from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+    to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+  },
+  "-moz-border-left-colors": {
+    from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+    to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+  },
+  "-moz-border-right-colors": {
+    from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+    to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+  },
+  "-moz-border-top-colors": {
+    from: "rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0) rgb(255, 0, 0)",
+    to: "rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0) rgb(0, 255, 0)"
+  },
+  "-moz-box-align": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/box-align
+    from: "center",
+    to: "stretch"
+  },
+  "-moz-box-direction": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/box-direction
+    from: "reverse",
+    to: "normal"
+  },
+  "-moz-box-ordinal-group": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/box-ordinal-group
+    from: "1",
+    to: "5"
+  },
+  "-moz-box-orient": {
+    // https://www.w3.org/TR/css-flexbox-1/
+    from: "horizontal",
+    to: "vertical"
+  },
+  "-moz-box-pack": {
+    // https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/#propdef-box-pack
+    from: "center",
+    to: "end"
+  },
+  "-moz-float-edge": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/-moz-float-edge
+    from: "margin-box",
+    to: "content-box"
+  },
+  "-moz-force-broken-image-icon": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/-moz-force-broken-image-icon
+    from: "1",
+    to: "5"
+  },
+  "image-rendering": {
+    // https://drafts.csswg.org/css-images-3/#propdef-image-rendering
+    from: "-moz-crisp-edges",
+    to: "auto"
+  },
+  "-moz-stack-sizing": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/-moz-stack-sizing
+    from: "ignore",
+    to: "stretch-to-fit"
+  },
+  "-moz-tab-size": {
+    // https://drafts.csswg.org/css-text-3/#propdef-tab-size
+    from: "1",
+    to: "5"
+  },
+  "-moz-text-size-adjust": {
+    // https://drafts.csswg.org/css-size-adjust/#propdef-text-size-adjust
+    from: "none",
+    to: "auto"
+  },
+  "-webkit-text-stroke-width": {
+    // https://compat.spec.whatwg.org/#propdef--webkit-text-stroke-width
+    from: "10px",
+    to: "50px"
+  }
+}
+
+for (let property in gMozillaSpecificProperties) {
+  const testData = gMozillaSpecificProperties[property];
+  const from = testData.from;
+  const to = testData.to;
+  const idlName = propertyToIDL(property);
+  const keyframes = {};
+  keyframes[idlName] = [from, to];
+
+  test(t => {
+    const div = addDiv(t);
+    const animation = div.animate(keyframes,
+                                  { duration: 1000, fill: "both" });
+    testAnimationSamples(animation, idlName,
+                         [{ time: 0,    expected: from.toLowerCase() },
+                          { time: 499,  expected: from.toLowerCase() },
+                          { time: 500,  expected: to.toLowerCase() },
+                          { time: 1000, expected: to.toLowerCase() }]);
+  }, property + " should animate between '"
+   + from + "' and '" + to + "' with linear easing");
+
+  test(function(t) {
+    // Easing: http://cubic-bezier.com/#.68,0,1,.01
+    // With this curve, we don't reach the 50% point until about 95% of
+    // the time has expired.
+    const div = addDiv(t);
+    const animation = div.animate(keyframes,
+                                  { duration: 1000, fill: "both",
+                                    easing: "cubic-bezier(0.68,0,1,0.01)" });
+    testAnimationSamples(animation, idlName,
+                         [{ time: 0,    expected: from.toLowerCase() },
+                          { time: 940,  expected: from.toLowerCase() },
+                          { time: 960,  expected: to.toLowerCase() }]);
+  }, property + " should animate between '"
+   + from + "' and '" + to + "' with effect easing");
+
+  test(function(t) {
+    // Easing: http://cubic-bezier.com/#.68,0,1,.01
+    // With this curve, we don't reach the 50% point until about 95% of
+    // the time has expired.
+    keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
+    const div = addDiv(t);
+    const animation = div.animate(keyframes,
+                                  { duration: 1000, fill: "both" });
+    testAnimationSamples(animation, idlName,
+                         [{ time: 0,    expected: from.toLowerCase() },
+                          { time: 940,  expected: from.toLowerCase() },
+                          { time: 960,  expected: to.toLowerCase() }]);
+  }, property + " should animate between '"
+   + from + "' and '" + to + "' with keyframe easing");
+}
+
+function propertyToIDL(property) {
+  var prefixMatch = property.match(/^-(\w+)-/);
+  if (prefixMatch) {
+    var prefix = prefixMatch[1] === "moz" ? "Moz" : prefixMatch[1];
+    property = prefix + property.substring(prefixMatch[0].length - 1);
+  }
+  // https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
+  return property.replace(/-([a-z])/gi, function(str, group) {
+    return group.toUpperCase();
+  });
+}
+
+function testAnimationSamples(animation, idlName, testSamples) {
+  const target = animation.effect.target;
+  testSamples.forEach(testSample => {
+    animation.currentTime = testSample.time;
+    assert_equals(getComputedStyle(target)[idlName], testSample.expected,
+                  "The value should be " + testSample.expected +
+                  " at " + testSample.time + "ms");
+  });
+}
+
+done();
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/mozilla/test_discrete-animations.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({explicit_done: true});
+SpecialPowers.pushPrefEnv(
+  { "set": [
+    ["dom.animations-api.core.enabled", true],
+    ["layout.css.osx-font-smoothing.enabled", true],
+    ["layout.css.prefixes.webkit", true]
+  ] },
+  function() {
+    window.open("file_discrete-animations.html");
+  });
+</script>
new file mode 100644
--- /dev/null
+++ b/dom/base/Pose.cpp
@@ -0,0 +1,94 @@
+/* -*- 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 "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/dom/PoseBinding.h"
+#include "mozilla/dom/Pose.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(Pose)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Pose)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+  tmp->mPosition = nullptr;
+  tmp->mLinearVelocity = nullptr;
+  tmp->mLinearAcceleration = nullptr;
+  tmp->mOrientation = nullptr;
+  tmp->mAngularVelocity = nullptr;
+  tmp->mAngularAcceleration = nullptr;
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Pose)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Pose)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPosition)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearVelocity)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearAcceleration)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mOrientation)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularVelocity)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularAcceleration)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(Pose, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(Pose, Release)
+
+
+Pose::Pose(nsISupports* aParent)
+  : mParent(aParent),
+    mPosition(nullptr),
+    mLinearVelocity(nullptr),
+    mLinearAcceleration(nullptr),
+    mOrientation(nullptr),
+    mAngularVelocity(nullptr),
+    mAngularAcceleration(nullptr)
+{
+  mozilla::HoldJSObjects(this);
+}
+
+Pose::~Pose()
+{
+  mozilla::DropJSObjects(this);
+}
+
+nsISupports*
+Pose::GetParentObject() const
+{
+  return mParent;
+}
+
+void
+Pose::SetFloat32Array(JSContext* aJSContext, JS::MutableHandle<JSObject*> aRetVal,
+                      JS::Heap<JSObject*>& aObj, float* aVal, uint32_t sizeOfVal,
+                      bool bCreate, ErrorResult& aRv)
+{
+  if (bCreate) {
+    aObj = Float32Array::Create(aJSContext, this,
+                                 sizeOfVal, aVal);
+    if (!aObj) {
+      aRv.NoteJSContextException(aJSContext);
+      return;
+    }
+  }
+
+  aRetVal.set(aObj);
+}
+
+/* virtual */ JSObject*
+Pose::WrapObject(JSContext* aJSContext, JS::Handle<JSObject*> aGivenProto)
+{
+  return PoseBinding::Wrap(aJSContext, this, aGivenProto);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/Pose.h
@@ -0,0 +1,67 @@
+/* -*- 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_dom_Pose_h
+#define mozilla_dom_Pose_h
+
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace dom {
+
+class Pose : public nsWrapperCache
+{
+public:
+  explicit Pose(nsISupports* aParent);
+
+  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(Pose)
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(Pose)
+
+  nsISupports* GetParentObject() const;
+
+  virtual void GetPosition(JSContext* aJSContext,
+                           JS::MutableHandle<JSObject*> aRetval,
+                           ErrorResult& aRv) = 0;
+  virtual void GetLinearVelocity(JSContext* aJSContext,
+                                 JS::MutableHandle<JSObject*> aRetval,
+                                 ErrorResult& aRv) = 0;
+  virtual void GetLinearAcceleration(JSContext* aJSContext,
+                                     JS::MutableHandle<JSObject*> aRetval,
+                                     ErrorResult& aRv) = 0;
+  virtual void GetOrientation(JSContext* aJSContext,
+                              JS::MutableHandle<JSObject*> aRetval,
+                              ErrorResult& aRv) = 0;
+  virtual void GetAngularVelocity(JSContext* aJSContext,
+                                  JS::MutableHandle<JSObject*> aRetval,
+                                  ErrorResult& aRv) = 0;
+  virtual void GetAngularAcceleration(JSContext* aJSContext,
+                                      JS::MutableHandle<JSObject*> aRetval,
+                                      ErrorResult& aRv) = 0;
+
+  virtual JSObject* WrapObject(JSContext* aJSContext,
+                               JS::Handle<JSObject*> aGivenProto) override;
+
+protected:
+  virtual ~Pose();
+
+  void SetFloat32Array(JSContext* aJSContext, JS::MutableHandle<JSObject*> aRetVal,
+                       JS::Heap<JSObject*>& aObj, float* aVal, uint32_t sizeOfVal,
+                       bool bCreate, ErrorResult& aRv);
+
+  nsCOMPtr<nsISupports> mParent;
+
+  JS::Heap<JSObject*> mPosition;
+  JS::Heap<JSObject*> mLinearVelocity;
+  JS::Heap<JSObject*> mLinearAcceleration;
+  JS::Heap<JSObject*> mOrientation;
+  JS::Heap<JSObject*> mAngularVelocity;
+  JS::Heap<JSObject*> mAngularAcceleration;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_Pose_h
--- a/dom/base/Timeout.cpp
+++ b/dom/base/Timeout.cpp
@@ -4,17 +4,16 @@
  * 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 "Timeout.h"
 
 #include "nsGlobalWindow.h"
 #include "nsITimeoutHandler.h"
 #include "nsITimer.h"
-#include "nsPIDOMWindow.h"
 
 namespace mozilla {
 namespace dom {
 
 Timeout::Timeout()
   : mCleared(false),
     mRunning(false),
     mIsInterval(false),
--- a/dom/base/Timeout.h
+++ b/dom/base/Timeout.h
@@ -6,21 +6,23 @@
 
 #ifndef mozilla_dom_timeout_h
 #define mozilla_dom_timeout_h
 
 #include "mozilla/LinkedList.h"
 #include "mozilla/TimeStamp.h"
 #include "nsCOMPtr.h"
 #include "nsCycleCollectionParticipant.h"
+#include "nsPIDOMWindow.h"
 
 class nsGlobalWindow;
 class nsIPrincipal;
 class nsITimeoutHandler;
 class nsITimer;
+class nsIEventTarget;
 
 namespace mozilla {
 namespace dom {
 
 /*
  * Timeout struct that holds information about each script
  * timeout.  Holds a strong reference to an nsITimeoutHandler, which
  * abstracts the language specific cruft.
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -196,16 +196,17 @@ EXPORTS.mozilla.dom += [
     'MutableBlobStorage.h',
     'MutableBlobStreamListener.h',
     'NameSpaceConstants.h',
     'Navigator.h',
     'NodeInfo.h',
     'NodeInfoInlines.h',
     'NodeIterator.h',
     'PartialSHistory.h',
+    'Pose.h',
     'ProcessGlobal.h',
     'ResponsiveImageSelector.h',
     'SameProcessMessageQueue.h',
     'ScreenOrientation.h',
     'ScriptSettings.h',
     'ShadowRoot.h',
     'StructuredCloneHolder.h',
     'StructuredCloneTags.h',
@@ -340,16 +341,17 @@ UNIFIED_SOURCES += [
     'nsViewportInfo.cpp',
     'nsWindowMemoryReporter.cpp',
     'nsWindowRoot.cpp',
     'nsWrapperCache.cpp',
     'nsXHTMLContentSerializer.cpp',
     'nsXMLContentSerializer.cpp',
     'nsXMLNameSpaceMap.cpp',
     'PartialSHistory.cpp',
+    'Pose.cpp',
     'PostMessageEvent.cpp',
     'ProcessGlobal.cpp',
     'ResponsiveImageSelector.cpp',
     'SameProcessMessageQueue.cpp',
     'ScreenOrientation.cpp',
     'ScriptSettings.cpp',
     'ShadowRoot.cpp',
     'StructuredCloneHolder.cpp',
--- a/dom/gamepad/Gamepad.cpp
+++ b/dom/gamepad/Gamepad.cpp
@@ -16,17 +16,17 @@ namespace dom {
 NS_IMPL_CYCLE_COLLECTING_ADDREF(Gamepad)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(Gamepad)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Gamepad)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
-NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Gamepad, mParent, mButtons)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Gamepad, mParent, mButtons, mPose)
 
 void
 Gamepad::UpdateTimestamp()
 {
   nsCOMPtr<nsPIDOMWindowInner> newWindow(do_QueryInterface(mParent));
   if(newWindow) {
     Performance* perf = newWindow->GetPerformance();
     if (perf) {
@@ -47,16 +47,17 @@ Gamepad::Gamepad(nsISupports* aParent,
     mButtons(aNumButtons),
     mAxes(aNumAxes),
     mTimestamp(0)
 {
   for (unsigned i = 0; i < aNumButtons; i++) {
     mButtons.InsertElementAt(i, new GamepadButton(mParent));
   }
   mAxes.InsertElementsAt(0, aNumAxes, 0.0f);
+  mPose = new GamepadPose(aParent);
   UpdateTimestamp();
 }
 
 void
 Gamepad::SetIndex(uint32_t aIndex)
 {
   mIndex = aIndex;
 }
@@ -83,36 +84,51 @@ Gamepad::SetAxis(uint32_t aAxis, double 
   if (mAxes[aAxis] != aValue) {
     mAxes[aAxis] = aValue;
     GamepadBinding::ClearCachedAxesValue(this);
   }
   UpdateTimestamp();
 }
 
 void
+Gamepad::SetPose(const GamepadPoseState& aPose)
+{
+  mPose->SetPoseState(aPose);
+}
+
+void
 Gamepad::SyncState(Gamepad* aOther)
 {
+  const char* kGamepadExtEnabledPref = "dom.gamepad.extensions.enabled";
+
   if (mButtons.Length() != aOther->mButtons.Length() ||
       mAxes.Length() != aOther->mAxes.Length()) {
     return;
   }
 
   mConnected = aOther->mConnected;
   for (uint32_t i = 0; i < mButtons.Length(); ++i) {
     mButtons[i]->SetPressed(aOther->mButtons[i]->Pressed());
     mButtons[i]->SetValue(aOther->mButtons[i]->Value());
   }
+
   bool changed = false;
   for (uint32_t i = 0; i < mAxes.Length(); ++i) {
     changed = changed || (mAxes[i] != aOther->mAxes[i]);
     mAxes[i] = aOther->mAxes[i];
   }
   if (changed) {
     GamepadBinding::ClearCachedAxesValue(this);
   }
+
+  if (Preferences::GetBool(kGamepadExtEnabledPref)) {
+    MOZ_ASSERT(aOther->GetPose());
+    mPose->SetPoseState(aOther->GetPose()->GetPoseState());
+  }
+
   UpdateTimestamp();
 }
 
 already_AddRefed<Gamepad>
 Gamepad::Clone(nsISupports* aParent)
 {
   RefPtr<Gamepad> out =
     new Gamepad(aParent, mID, mIndex, mMapping,
--- a/dom/gamepad/Gamepad.h
+++ b/dom/gamepad/Gamepad.h
@@ -5,16 +5,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_gamepad_Gamepad_h
 #define mozilla_dom_gamepad_Gamepad_h
 
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/GamepadBinding.h"
 #include "mozilla/dom/GamepadButton.h"
+#include "mozilla/dom/GamepadPose.h"
 #include "mozilla/dom/Performance.h"
 #include <stdint.h>
 #include "nsCOMPtr.h"
 #include "nsString.h"
 #include "nsTArray.h"
 #include "nsWrapperCache.h"
 
 namespace mozilla {
@@ -44,16 +45,17 @@ public:
           uint32_t aNumButtons, uint32_t aNumAxes);
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(Gamepad)
 
   void SetConnected(bool aConnected);
   void SetButton(uint32_t aButton, bool aPressed, double aValue);
   void SetAxis(uint32_t aAxis, double aValue);
   void SetIndex(uint32_t aIndex);
+  void SetPose(const GamepadPoseState& aPose);
 
   // Make the state of this gamepad equivalent to other.
   void SyncState(Gamepad* aOther);
 
   // Return a new Gamepad containing the same data as this object,
   // parented to aParent.
   already_AddRefed<Gamepad> Clone(nsISupports* aParent);
 
@@ -94,16 +96,21 @@ public:
     aButtons = mButtons;
   }
 
   void GetAxes(nsTArray<double>& aAxes) const
   {
     aAxes = mAxes;
   }
 
+  GamepadPose* GetPose() const
+  {
+    return mPose;
+  }
+
 private:
   virtual ~Gamepad() {}
   void UpdateTimestamp();
 
 protected:
   nsCOMPtr<nsISupports> mParent;
   nsString mID;
   uint32_t mIndex;
@@ -113,14 +120,15 @@ protected:
 
   // true if this gamepad is currently connected.
   bool mConnected;
 
   // Current state of buttons, axes.
   nsTArray<RefPtr<GamepadButton>> mButtons;
   nsTArray<double> mAxes;
   DOMHighResTimeStamp mTimestamp;
+  RefPtr<GamepadPose> mPose;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_gamepad_Gamepad_h
--- a/dom/gamepad/GamepadManager.cpp
+++ b/dom/gamepad/GamepadManager.cpp
@@ -401,16 +401,59 @@ GamepadManager::FireAxisMoveEvent(EventT
 
   event->SetTrusted(true);
 
   bool defaultActionEnabled = true;
   aTarget->DispatchEvent(event, &defaultActionEnabled);
 }
 
 void
+GamepadManager::NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType,
+                             const GamepadPoseState& aPose)
+{
+  if (mShuttingDown) {
+    return;
+  }
+
+  uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);
+
+  RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
+  if (!gamepad) {
+    return;
+  }
+  gamepad->SetPose(aPose);
+
+  // Hold on to listeners in a separate array because firing events
+  // can mutate the mListeners array.
+  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
+  MOZ_ASSERT(!listeners.IsEmpty());
+
+  for (uint32_t i = 0; i < listeners.Length(); i++) {
+
+    MOZ_ASSERT(listeners[i]->IsInnerWindow());
+
+    // Only send events to non-background windows
+    if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
+        listeners[i]->GetOuterWindow()->IsBackground()) {
+      continue;
+    }
+
+    bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);
+
+    RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
+    if (listenerGamepad) {
+      listenerGamepad->SetPose(aPose);
+      if (firstTime) {
+        FireConnectionEvent(listeners[i], listenerGamepad, true);
+      }
+    }
+  }
+}
+
+void
 GamepadManager::NewConnectionEvent(uint32_t aIndex, bool aConnected)
 {
   if (mShuttingDown) {
     return;
   }
 
   RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
   if (!gamepad) {
@@ -606,16 +649,21 @@ GamepadManager::Update(const GamepadChan
                    a.pressed(), a.value());
     return;
   }
   if (aEvent.type() == GamepadChangeEvent::TGamepadAxisInformation) {
     const GamepadAxisInformation& a = aEvent.get_GamepadAxisInformation();
     NewAxisMoveEvent(a.index(), a.service_type(), a.axis(), a.value());
     return;
   }
+  if (aEvent.type() == GamepadChangeEvent::TGamepadPoseInformation) {
+    const GamepadPoseInformation& a = aEvent.get_GamepadPoseInformation();
+    NewPoseEvent(a.index(), a.service_type(), a.pose_state());
+    return;
+  }
 
   MOZ_CRASH("We shouldn't be here!");
 
 }
 
 //Override nsIIPCBackgroundChildCreateCallback
 void
 GamepadManager::ActorCreated(PBackgroundChild *aActor)
--- a/dom/gamepad/GamepadManager.h
+++ b/dom/gamepad/GamepadManager.h
@@ -64,16 +64,21 @@ class GamepadManager final : public nsIO
                       bool aPressed, double aValue);
 
   // Update the state of |aAxis| for the gamepad at |aIndex| for all
   // windows that are listening and visible, and fire a gamepadaxismove
   // event at them as well.
   void NewAxisMoveEvent(uint32_t aIndex, GamepadServiceType aServiceType,
                         uint32_t aAxis, double aValue);
 
+  // Update the state of |aState| for the gamepad at |aIndex| for all
+  // windows that are listening and visible.
+  void NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType,
+                    const GamepadPoseState& aState);
+
   // Synchronize the state of |aGamepad| to match the gamepad stored at |aIndex|
   void SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad);
 
   // Returns gamepad object if index exists, null otherwise
   already_AddRefed<Gamepad> GetGamepad(uint32_t aIndex) const;
 
   // Receive GamepadChangeEvent messages from parent process to fire DOM events
   void Update(const GamepadChangeEvent& aGamepadEvent);
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/GamepadPose.cpp
@@ -0,0 +1,120 @@
+/* -*- 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 "nsWrapperCache.h"
+
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/GamepadPoseBinding.h"
+#include "mozilla/dom/GamepadPose.h"
+
+namespace mozilla {
+namespace dom {
+
+GamepadPose::GamepadPose(nsISupports* aParent, const GamepadPoseState& aState)
+  : Pose(aParent),
+    mPoseState(aState)
+{
+  mozilla::HoldJSObjects(this);
+}
+
+GamepadPose::GamepadPose(nsISupports* aParent)
+  : Pose(aParent)
+{
+  mozilla::HoldJSObjects(this);
+  mPoseState.Clear();
+}
+
+GamepadPose::~GamepadPose()
+{
+  mozilla::DropJSObjects(this);
+}
+
+/* virtual */ JSObject*
+GamepadPose::WrapObject(JSContext* aJSContext, JS::Handle<JSObject*> aGivenProto)
+{
+  return GamepadPoseBinding::Wrap(aJSContext, this, aGivenProto);
+}
+
+bool
+GamepadPose::HasOrientation() const
+{
+  return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position);
+}
+
+bool
+GamepadPose::HasPosition() const
+{
+  return bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation);
+}
+
+void
+GamepadPose::GetPosition(JSContext* aJSContext,
+                         JS::MutableHandle<JSObject*> aRetval,
+                         ErrorResult& aRv)
+{
+  SetFloat32Array(aJSContext, aRetval, mPosition, mPoseState.position, 3,
+    bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position), aRv);
+}
+
+void
+GamepadPose::GetLinearVelocity(JSContext* aJSContext,
+                               JS::MutableHandle<JSObject*> aRetval,
+                               ErrorResult& aRv)
+{
+  SetFloat32Array(aJSContext, aRetval, mLinearVelocity, mPoseState.linearVelocity, 3,
+    bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Position), aRv);
+}
+
+void
+GamepadPose::GetLinearAcceleration(JSContext* aJSContext,
+                                   JS::MutableHandle<JSObject*> aRetval,
+                                   ErrorResult& aRv)
+{
+  SetFloat32Array(aJSContext, aRetval, mLinearAcceleration, mPoseState.linearAcceleration, 3,
+    bool(mPoseState.flags & GamepadCapabilityFlags::Cap_LinearAcceleration), aRv);
+}
+
+void
+GamepadPose::GetOrientation(JSContext* aJSContext,
+                            JS::MutableHandle<JSObject*> aRetval,
+                            ErrorResult& aRv)
+{
+  SetFloat32Array(aJSContext, aRetval, mOrientation, mPoseState.orientation, 4,
+    bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation), aRv);
+}
+
+void
+GamepadPose::GetAngularVelocity(JSContext* aJSContext,
+                                JS::MutableHandle<JSObject*> aRetval,
+                                ErrorResult& aRv)
+{
+  SetFloat32Array(aJSContext, aRetval, mAngularVelocity, mPoseState.angularVelocity, 3,
+    bool(mPoseState.flags & GamepadCapabilityFlags::Cap_Orientation), aRv);
+}
+
+void
+GamepadPose::GetAngularAcceleration(JSContext* aJSContext,
+                                    JS::MutableHandle<JSObject*> aRetval,
+                                    ErrorResult& aRv)
+{
+  SetFloat32Array(aJSContext, aRetval, mAngularAcceleration, mPoseState.angularAcceleration, 3,
+    bool(mPoseState.flags & GamepadCapabilityFlags::Cap_AngularAcceleration), aRv);
+}
+
+void
+GamepadPose::SetPoseState(const GamepadPoseState& aPose)
+{
+  mPoseState = aPose;
+}
+
+const GamepadPoseState&
+GamepadPose::GetPoseState()
+{
+  return mPoseState;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/GamepadPose.h
@@ -0,0 +1,58 @@
+/* -*- 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_dom_gamepad_GamepadPose_h
+#define mozilla_dom_gamepad_GamepadPose_h
+
+#include "mozilla/TypedEnumBits.h"
+#include "mozilla/dom/Pose.h"
+#include "mozilla/dom/GamepadPoseState.h"
+
+namespace mozilla {
+namespace dom {
+
+class GamepadPose final : public Pose
+{
+public:
+  GamepadPose(nsISupports* aParent, const GamepadPoseState& aState);
+  explicit GamepadPose(nsISupports* aParent);
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+  bool HasOrientation() const;
+  bool HasPosition() const;
+  virtual void GetPosition(JSContext* aJSContext,
+                           JS::MutableHandle<JSObject*> aRetval,
+                           ErrorResult& aRv) override;
+  virtual void GetLinearVelocity(JSContext* aJSContext,
+                                 JS::MutableHandle<JSObject*> aRetval,
+                                 ErrorResult& aRv) override;
+  virtual void GetLinearAcceleration(JSContext* aJSContext,
+                                     JS::MutableHandle<JSObject*> aRetval,
+                                     ErrorResult& aRv) override;
+  virtual void GetOrientation(JSContext* aJSContext,
+                              JS::MutableHandle<JSObject*> aRetval,
+                              ErrorResult& aRv) override;
+  virtual void GetAngularVelocity(JSContext* aJSContext,
+                                  JS::MutableHandle<JSObject*> aRetval,
+                                  ErrorResult& aRv) override;
+  virtual void GetAngularAcceleration(JSContext* aJSContext,
+                                      JS::MutableHandle<JSObject*> aRetval,
+                                      ErrorResult& aRv) override;
+  void SetPoseState(const GamepadPoseState& aPose);
+  const GamepadPoseState& GetPoseState();
+
+private:
+  virtual ~GamepadPose();
+
+  nsCOMPtr<nsISupports> mParent;
+  GamepadPoseState mPoseState;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_gamepad_GamepadPose_h
new file mode 100644
--- /dev/null
+++ b/dom/gamepad/GamepadPoseState.h
@@ -0,0 +1,88 @@
+
+#ifndef mozilla_dom_gamepad_GamepadPoseState_h_
+#define mozilla_dom_gamepad_GamepadPoseState_h_
+
+namespace mozilla{
+namespace dom{
+
+enum class GamepadCapabilityFlags : uint16_t {
+  Cap_None = 0,
+  /**
+   * Cap_Position is set if the Gamepad is capable of tracking its position.
+   */
+  Cap_Position = 1 << 1,
+  /**
+    * Cap_Orientation is set if the Gamepad is capable of tracking its orientation.
+    */
+  Cap_Orientation = 1 << 2,
+  /**
+   * Cap_AngularAcceleration is set if the Gamepad is capable of tracking its
+   * angular acceleration.
+   */
+  Cap_AngularAcceleration = 1 << 3,
+  /**
+   * Cap_LinearAcceleration is set if the Gamepad is capable of tracking its
+   * linear acceleration.
+   */
+  Cap_LinearAcceleration = 1 << 4,
+  /**
+   * Cap_All used for validity checking during IPC serialization
+   */
+  Cap_All = (1 << 5) - 1
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(GamepadCapabilityFlags)
+
+struct GamepadPoseState
+{
+  GamepadCapabilityFlags flags;
+  float orientation[4];
+  float position[3];
+  float angularVelocity[3];
+  float angularAcceleration[3];
+  float linearVelocity[3];
+  float linearAcceleration[3];
+
+  GamepadPoseState()
+  {
+    Clear();
+  }
+
+  bool operator==(const GamepadPoseState& aPose) const
+  {
+    return flags == aPose.flags
+           && orientation[0] == aPose.orientation[0]
+           && orientation[1] == aPose.orientation[1]
+           && orientation[2] == aPose.orientation[2]
+           && orientation[3] == aPose.orientation[3]
+           && position[0] == aPose.position[0]
+           && position[1] == aPose.position[1]
+           && position[2] == aPose.position[2]
+           && angularVelocity[0] == aPose.angularVelocity[0]
+           && angularVelocity[1] == aPose.angularVelocity[1]
+           && angularVelocity[2] == aPose.angularVelocity[2]
+           && angularAcceleration[0] == aPose.angularAcceleration[0]
+           && angularAcceleration[1] == aPose.angularAcceleration[1]
+           && angularAcceleration[2] == aPose.angularAcceleration[2]
+           && linearVelocity[0] == aPose.linearVelocity[0]
+           && linearVelocity[1] == aPose.linearVelocity[1]
+           && linearVelocity[2] == aPose.linearVelocity[2]
+           && linearAcceleration[0] == aPose.linearAcceleration[0]
+           && linearAcceleration[1] == aPose.linearAcceleration[1]
+           && linearAcceleration[2] == aPose.linearAcceleration[2];
+  }
+
+  bool operator!=(const GamepadPoseState& aPose) const
+  {
+    return !(*this == aPose);
+  }
+
+  void Clear() {
+    memset(this, 0, sizeof(GamepadPoseState));
+  }
+};
+
+}// namespace dom
+}// namespace mozilla
+
+#endif // mozilla_dom_gamepad_GamepadPoseState_h_
\ No newline at end of file
--- a/dom/gamepad/ipc/GamepadEventTypes.ipdlh
+++ b/dom/gamepad/ipc/GamepadEventTypes.ipdlh
@@ -1,13 +1,14 @@
 /* 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/. */
 
 using mozilla::dom::GamepadServiceType from "mozilla/dom/GamepadMessageUtils.h";
+using mozilla::dom::GamepadPoseState from "mozilla/dom/GamepadMessageUtils.h";
 
 
 namespace mozilla {
 namespace dom {
 
 struct GamepadAdded {
   nsString id;
   uint32_t index;
@@ -35,17 +36,24 @@ struct GamepadAxisInformation {
 struct GamepadButtonInformation {
   uint32_t index;
   GamepadServiceType service_type;
   uint32_t button;
   bool pressed;
   double value;
 };
 
+struct GamepadPoseInformation {
+  uint32_t index;
+  GamepadServiceType service_type;
+  GamepadPoseState pose_state;
+};
+
 union GamepadChangeEvent {
   GamepadAdded;
   GamepadRemoved;
   GamepadAxisInformation;
   GamepadButtonInformation;
+  GamepadPoseInformation;
 };
 
 } // namespace dom
 } // namespace mozilla
\ No newline at end of file
--- a/dom/gamepad/ipc/GamepadMessageUtils.h
+++ b/dom/gamepad/ipc/GamepadMessageUtils.h
@@ -1,18 +1,82 @@
 
 #ifndef mozilla_dom_gamepad_GamepadMessageUtils_h
 #define mozilla_dom_gamepad_GamepadMessageUtils_h
 
 #include "ipc/IPCMessageUtils.h"
 #include "mozilla/dom/GamepadServiceType.h"
+#include "mozilla/dom/GamepadPoseState.h"
 
 namespace IPC {
 
 template<>
 struct ParamTraits<mozilla::dom::GamepadServiceType> :
   public ContiguousEnumSerializer<mozilla::dom::GamepadServiceType,
                                   mozilla::dom::GamepadServiceType(0),
                                   mozilla::dom::GamepadServiceType(
                                   mozilla::dom::GamepadServiceType::NumGamepadServiceType)> {};
+
+template<>
+struct ParamTraits<mozilla::dom::GamepadCapabilityFlags> :
+  public BitFlagsEnumSerializer<mozilla::dom::GamepadCapabilityFlags,
+                                mozilla::dom::GamepadCapabilityFlags::Cap_All> {};
+
+template <>
+struct ParamTraits<mozilla::dom::GamepadPoseState>
+{
+  typedef mozilla::dom::GamepadPoseState paramType;
+
+  static void Write(Message* aMsg, const paramType& aParam)
+  {
+    WriteParam(aMsg, aParam.flags);
+    WriteParam(aMsg, aParam.orientation[0]);
+    WriteParam(aMsg, aParam.orientation[1]);
+    WriteParam(aMsg, aParam.orientation[2]);
+    WriteParam(aMsg, aParam.orientation[3]);
+    WriteParam(aMsg, aParam.position[0]);
+    WriteParam(aMsg, aParam.position[1]);
+    WriteParam(aMsg, aParam.position[2]);
+    WriteParam(aMsg, aParam.angularVelocity[0]);
+    WriteParam(aMsg, aParam.angularVelocity[1]);
+    WriteParam(aMsg, aParam.angularVelocity[2]);
+    WriteParam(aMsg, aParam.angularAcceleration[0]);
+    WriteParam(aMsg, aParam.angularAcceleration[1]);
+    WriteParam(aMsg, aParam.angularAcceleration[2]);
+    WriteParam(aMsg, aParam.linearVelocity[0]);
+    WriteParam(aMsg, aParam.linearVelocity[1]);
+    WriteParam(aMsg, aParam.linearVelocity[2]);
+    WriteParam(aMsg, aParam.linearAcceleration[0]);
+    WriteParam(aMsg, aParam.linearAcceleration[1]);
+    WriteParam(aMsg, aParam.linearAcceleration[2]);
+  }
+
+  static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+  {
+    if (!ReadParam(aMsg, aIter, &(aResult->flags)) ||
+        !ReadParam(aMsg, aIter, &(aResult->orientation[0])) ||
+        !ReadParam(aMsg, aIter, &(aResult->orientation[1])) ||
+        !ReadParam(aMsg, aIter, &(aResult->orientation[2])) ||
+        !ReadParam(aMsg, aIter, &(aResult->orientation[3])) ||
+        !ReadParam(aMsg, aIter, &(aResult->position[0])) ||
+        !ReadParam(aMsg, aIter, &(aResult->position[1])) ||
+        !ReadParam(aMsg, aIter, &(aResult->position[2])) ||
+        !ReadParam(aMsg, aIter, &(aResult->angularVelocity[0])) ||
+        !ReadParam(aMsg, aIter, &(aResult->angularVelocity[1])) ||
+        !ReadParam(aMsg, aIter, &(aResult->angularVelocity[2])) ||
+        !ReadParam(aMsg, aIter, &(aResult->angularAcceleration[0])) ||
+        !ReadParam(aMsg, aIter, &(aResult->angularAcceleration[1])) ||
+        !ReadParam(aMsg, aIter, &(aResult->angularAcceleration[2])) ||
+        !ReadParam(aMsg, aIter, &(aResult->linearVelocity[0])) ||
+        !ReadParam(aMsg, aIter, &(aResult->linearVelocity[1])) ||
+        !ReadParam(aMsg, aIter, &(aResult->linearVelocity[2])) ||
+        !ReadParam(aMsg, aIter, &(aResult->linearAcceleration[0])) ||
+        !ReadParam(aMsg, aIter, &(aResult->linearAcceleration[1])) ||
+        !ReadParam(aMsg, aIter, &(aResult->linearAcceleration[2]))) {
+      return false;
+    }
+    return true;
+  }
+};
+
 } // namespace IPC
 
 #endif // mozilla_dom_gamepad_GamepadMessageUtils_h
\ No newline at end of file
--- a/dom/gamepad/moz.build
+++ b/dom/gamepad/moz.build
@@ -6,40 +6,43 @@
 
 IPDL_SOURCES += [
     'ipc/GamepadEventTypes.ipdlh',
     'ipc/PGamepadEventChannel.ipdl',
     'ipc/PGamepadTestChannel.ipdl'
 ]
 
 EXPORTS.mozilla.dom += [
+    'GamepadPoseState.h',
     'ipc/GamepadMessageUtils.h',
     'ipc/GamepadServiceType.h'
 ]
 
 if CONFIG['MOZ_GAMEPAD']:
     EXPORTS.mozilla.dom += [
         'Gamepad.h',
         'GamepadButton.h',
         'GamepadManager.h',
         'GamepadMonitoring.h',
         'GamepadPlatformService.h',
+        'GamepadPose.h',
         'GamepadServiceTest.h',
         'ipc/GamepadEventChannelChild.h',
         'ipc/GamepadEventChannelParent.h',
         'ipc/GamepadTestChannelChild.h',
         'ipc/GamepadTestChannelParent.h'
         ]
 
     UNIFIED_SOURCES = [
         'Gamepad.cpp',
         'GamepadButton.cpp',
         'GamepadManager.cpp',
         'GamepadMonitoring.cpp',
         'GamepadPlatformService.cpp',
+        'GamepadPose.cpp',
         'GamepadServiceTest.cpp',
         'ipc/GamepadEventChannelChild.cpp',
         'ipc/GamepadEventChannelParent.cpp',
         'ipc/GamepadTestChannelChild.cpp',
         'ipc/GamepadTestChannelParent.cpp'
         ]
 
     if CONFIG['MOZ_GAMEPAD_BACKEND'] == 'stub':
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -400,16 +400,18 @@ var interfaceNamesInGlobalScope =
     "GamepadAxisMoveEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "GamepadButtonEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "GamepadButton",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "GamepadEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "GamepadPose", release: false},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "HashChangeEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Headers",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "History",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "HTMLAllCollection",
 // IMPORTANT: Do not change this list without review from a DOM peer!
@@ -770,16 +772,18 @@ var interfaceNamesInGlobalScope =
     {name: "PointerEvent", nightly: true, desktop: true, disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PopStateEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PopupBlockedEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PopupBoxObject", xbl: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "Pose", release: false},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PresentationDeviceInfoManager",
      disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "Presentation", disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PresentationAvailability", disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PresentationConnection", disabled: true},
--- a/dom/vr/VRDisplay.cpp
+++ b/dom/vr/VRDisplay.cpp
@@ -251,174 +251,96 @@ NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(VRD
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(VRDisplayCapabilities, Release)
 
 JSObject*
 VRDisplayCapabilities::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return VRDisplayCapabilitiesBinding::Wrap(aCx, this, aGivenProto);
 }
 
-NS_IMPL_CYCLE_COLLECTION_CLASS(VRPose)
-
-NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(VRPose)
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
-  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
-  tmp->mPosition = nullptr;
-  tmp->mLinearVelocity = nullptr;
-  tmp->mLinearAcceleration = nullptr;
-  tmp->mOrientation = nullptr;
-  tmp->mAngularVelocity = nullptr;
-  tmp->mAngularAcceleration = nullptr;
-NS_IMPL_CYCLE_COLLECTION_UNLINK_END
-
-NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(VRPose)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
-NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
-
-NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(VRPose)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mPosition)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearVelocity)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLinearAcceleration)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mOrientation)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularVelocity)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mAngularAcceleration)
-NS_IMPL_CYCLE_COLLECTION_TRACE_END
-
-NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(VRPose, AddRef)
-NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(VRPose, Release)
-
 VRPose::VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState)
-  : mParent(aParent)
+  : Pose(aParent)
   , mVRState(aState)
-  , mPosition(nullptr)
-  , mLinearVelocity(nullptr)
-  , mLinearAcceleration(nullptr)
-  , mOrientation(nullptr)
-  , mAngularVelocity(nullptr)
-  , mAngularAcceleration(nullptr)
 {
   mFrameId = aState.inputFrameID;
   mozilla::HoldJSObjects(this);
 }
 
 VRPose::VRPose(nsISupports* aParent)
-  : mParent(aParent)
-  , mPosition(nullptr)
-  , mLinearVelocity(nullptr)
-  , mLinearAcceleration(nullptr)
-  , mOrientation(nullptr)
-  , mAngularVelocity(nullptr)
-  , mAngularAcceleration(nullptr)
+  : Pose(aParent)
 {
   mFrameId = 0;
   mVRState.Clear();
   mozilla::HoldJSObjects(this);
 }
 
 VRPose::~VRPose()
 {
   mozilla::DropJSObjects(this);
 }
 
 void
 VRPose::GetPosition(JSContext* aCx,
                     JS::MutableHandle<JSObject*> aRetval,
                     ErrorResult& aRv)
 {
-  if (!mPosition && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position) {
-    // Lazily create the Float32Array
-    mPosition = dom::Float32Array::Create(aCx, this, 3, mVRState.position);
-    if (!mPosition) {
-      aRv.NoteJSContextException(aCx);
-      return;
-    }
-  }
-  aRetval.set(mPosition);
+  SetFloat32Array(aCx, aRetval, mPosition, mVRState.position, 3,
+    !mPosition && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position),
+    aRv);
 }
 
 void
 VRPose::GetLinearVelocity(JSContext* aCx,
                           JS::MutableHandle<JSObject*> aRetval,
                           ErrorResult& aRv)
 {
-  if (!mLinearVelocity && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position) {
-    // Lazily create the Float32Array
-    mLinearVelocity = dom::Float32Array::Create(aCx, this, 3, mVRState.linearVelocity);
-    if (!mLinearVelocity) {
-      aRv.NoteJSContextException(aCx);
-      return;
-    }
-  }
-  aRetval.set(mLinearVelocity);
+  SetFloat32Array(aCx, aRetval, mLinearVelocity, mVRState.linearVelocity, 3,
+    !mLinearVelocity && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Position),
+    aRv);
 }
 
 void
 VRPose::GetLinearAcceleration(JSContext* aCx,
                               JS::MutableHandle<JSObject*> aRetval,
                               ErrorResult& aRv)
 {
-  if (!mLinearAcceleration && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_LinearAcceleration) {
-    // Lazily create the Float32Array
-    mLinearAcceleration = dom::Float32Array::Create(aCx, this, 3, mVRState.linearAcceleration);
-    if (!mLinearAcceleration) {
-      aRv.NoteJSContextException(aCx);
-      return;
-    }
-  }
-  aRetval.set(mLinearAcceleration);
+  SetFloat32Array(aCx, aRetval, mLinearAcceleration, mVRState.linearAcceleration, 3,
+    !mLinearAcceleration && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_LinearAcceleration),
+    aRv);
+
 }
 
 void
 VRPose::GetOrientation(JSContext* aCx,
                        JS::MutableHandle<JSObject*> aRetval,
                        ErrorResult& aRv)
 {
-  if (!mOrientation && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation) {
-    // Lazily create the Float32Array
-    mOrientation = dom::Float32Array::Create(aCx, this, 4, mVRState.orientation);
-    if (!mOrientation) {
-      aRv.NoteJSContextException(aCx);
-      return;
-    }
-  }
-  aRetval.set(mOrientation);
+  SetFloat32Array(aCx, aRetval, mOrientation, mVRState.orientation, 4,
+    !mOrientation && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation),
+    aRv);
 }
 
 void
 VRPose::GetAngularVelocity(JSContext* aCx,
                            JS::MutableHandle<JSObject*> aRetval,
                            ErrorResult& aRv)
 {
-  if (!mAngularVelocity && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation) {
-    // Lazily create the Float32Array
-    mAngularVelocity = dom::Float32Array::Create(aCx, this, 3, mVRState.angularVelocity);
-    if (!mAngularVelocity) {
-      aRv.NoteJSContextException(aCx);
-      return;
-    }
-  }
-  aRetval.set(mAngularVelocity);
+  SetFloat32Array(aCx, aRetval, mAngularVelocity, mVRState.angularVelocity, 3,
+    !mAngularVelocity && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_Orientation),
+    aRv);
 }
 
 void
 VRPose::GetAngularAcceleration(JSContext* aCx,
                                JS::MutableHandle<JSObject*> aRetval,
                                ErrorResult& aRv)
 {
-  if (!mAngularAcceleration && mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_AngularAcceleration) {
-    // Lazily create the Float32Array
-    mAngularAcceleration = dom::Float32Array::Create(aCx, this, 3, mVRState.angularAcceleration);
-    if (!mAngularAcceleration) {
-      aRv.NoteJSContextException(aCx);
-      return;
-    }
-  }
-  aRetval.set(mAngularAcceleration);
+  SetFloat32Array(aCx, aRetval, mAngularAcceleration, mVRState.angularAcceleration, 3,
+    !mAngularAcceleration && bool(mVRState.flags & gfx::VRDisplayCapabilityFlags::Cap_AngularAcceleration),
+    aRv);
 }
 
 JSObject*
 VRPose::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return VRPoseBinding::Wrap(aCx, this, aGivenProto);
 }
 
--- a/dom/vr/VRDisplay.h
+++ b/dom/vr/VRDisplay.h
@@ -10,21 +10,21 @@
 #include <stdint.h>
 
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/TypedArray.h"
 #include "mozilla/dom/VRDisplayBinding.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/dom/DOMPoint.h"
 #include "mozilla/dom/DOMRect.h"
+#include "mozilla/dom/Pose.h"
 
 #include "nsCOMPtr.h"
 #include "nsString.h"
 #include "nsTArray.h"
-#include "nsWrapperCache.h"
 
 #include "gfxVR.h"
 
 namespace mozilla {
 namespace gfx {
 class VRDisplayClient;
 class VRDisplayPresentation;
 struct VRFieldOfView;
@@ -90,64 +90,51 @@ public:
   uint32_t MaxLayers() const;
 
 protected:
   ~VRDisplayCapabilities() {}
   nsCOMPtr<nsISupports> mParent;
   gfx::VRDisplayCapabilityFlags mFlags;
 };
 
-class VRPose final : public nsWrapperCache
+class VRPose final : public Pose
 {
 
 public:
   VRPose(nsISupports* aParent, const gfx::VRHMDSensorState& aState);
   explicit VRPose(nsISupports* aParent);
 
-  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VRPose)
-  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(VRPose)
-
   uint32_t FrameID() const { return mFrameId; }
 
-  void GetPosition(JSContext* aCx,
-                   JS::MutableHandle<JSObject*> aRetval,
-                   ErrorResult& aRv);
-  void GetLinearVelocity(JSContext* aCx,
-                         JS::MutableHandle<JSObject*> aRetval,
-                         ErrorResult& aRv);
-  void GetLinearAcceleration(JSContext* aCx,
-                             JS::MutableHandle<JSObject*> aRetval,
-                             ErrorResult& aRv);
-  void GetOrientation(JSContext* aCx,
-                      JS::MutableHandle<JSObject*> aRetval,
-                      ErrorResult& aRv);
-  void GetAngularVelocity(JSContext* aCx,
-                          JS::MutableHandle<JSObject*> aRetval,
-                          ErrorResult& aRv);
-  void GetAngularAcceleration(JSContext* aCx,
+  virtual void GetPosition(JSContext* aCx,
+                           JS::MutableHandle<JSObject*> aRetval,
+                           ErrorResult& aRv) override;
+  virtual void GetLinearVelocity(JSContext* aCx,
+                                 JS::MutableHandle<JSObject*> aRetval,
+                                 ErrorResult& aRv) override;
+  virtual void GetLinearAcceleration(JSContext* aCx,
+                                     JS::MutableHandle<JSObject*> aRetval,
+                                     ErrorResult& aRv) override;
+  virtual void GetOrientation(JSContext* aCx,
                               JS::MutableHandle<JSObject*> aRetval,
-                              ErrorResult& aRv);
+                              ErrorResult& aRv) override;
+  virtual void GetAngularVelocity(JSContext* aCx,
+                                  JS::MutableHandle<JSObject*> aRetval,
+                                  ErrorResult& aRv) override;
+  virtual void GetAngularAcceleration(JSContext* aCx,
+                                      JS::MutableHandle<JSObject*> aRetval,
+                                      ErrorResult& aRv) override;
 
-  nsISupports* GetParentObject() const { return mParent; }
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
 protected:
   ~VRPose();
-  nsCOMPtr<nsISupports> mParent;
 
   uint32_t mFrameId;
   gfx::VRHMDSensorState mVRState;
-
-  JS::Heap<JSObject*> mPosition;
-  JS::Heap<JSObject*> mLinearVelocity;
-  JS::Heap<JSObject*> mLinearAcceleration;
-  JS::Heap<JSObject*> mOrientation;
-  JS::Heap<JSObject*> mAngularVelocity;
-  JS::Heap<JSObject*> mAngularAcceleration;
-
 };
 
 struct VRFrameInfo
 {
   VRFrameInfo();
 
   void Update(const gfx::VRDisplayInfo& aInfo,
               const gfx::VRHMDSensorState& aState,
--- a/dom/webidl/Gamepad.webidl
+++ b/dom/webidl/Gamepad.webidl
@@ -51,9 +51,15 @@ interface Gamepad {
    */
   [Pure, Cached, Frozen]
   readonly attribute sequence<double> axes;
 
   /**
    * Timestamp from when the data of this device was last updated.
    */
   readonly attribute DOMHighResTimeStamp timestamp;
+
+  /**
+   * The current pose of the device, a GamepadPose.
+   */
+  [Pref="dom.gamepad.extensions.enabled"]
+  readonly attribute GamepadPose? pose;
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/GamepadPose.webidl
@@ -0,0 +1,13 @@
+/* -*- 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/.
+ */
+
+[Pref="dom.gamepad.extensions.enabled",
+  HeaderFile="mozilla/dom/GamepadPose.h"]
+interface GamepadPose : Pose
+{
+  readonly attribute boolean hasOrientation;
+  readonly attribute boolean hasPosition;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/Pose.webidl
@@ -0,0 +1,23 @@
+/* -*- 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/.
+ */
+
+interface Pose
+{
+  /**
+   * position, linearVelocity, and linearAcceleration are 3-component vectors.
+   * position is relative to a sitting space. Transforming this point with
+   * VRStageParameters.sittingToStandingTransform converts this to standing space.
+   */
+  [Constant, Throws] readonly attribute Float32Array? position;
+  [Constant, Throws] readonly attribute Float32Array? linearVelocity;
+  [Constant, Throws] readonly attribute Float32Array? linearAcceleration;
+
+  /* orientation is a 4-entry array representing the components of a quaternion. */
+  [Constant, Throws] readonly attribute Float32Array? orientation;
+  /* angularVelocity and angularAcceleration are the components of 3-dimensional vectors. */
+  [Constant, Throws] readonly attribute Float32Array? angularVelocity;
+  [Constant, Throws] readonly attribute Float32Array? angularAcceleration;
+};
--- a/dom/webidl/VRDisplay.webidl
+++ b/dom/webidl/VRDisplay.webidl
@@ -111,31 +111,19 @@ interface VRStageParameters {
    * this rectangle.
    */
   readonly attribute float sizeX;
   readonly attribute float sizeZ;
 };
 
 [Pref="dom.vr.enabled",
  HeaderFile="mozilla/dom/VRDisplay.h"]
-interface VRPose {
-  /**
-   * position, linearVelocity, and linearAcceleration are 3-component vectors.
-   * position is relative to a sitting space. Transforming this point with
-   * VRStageParameters.sittingToStandingTransform converts this to standing space.
-   */
-  [Constant, Throws] readonly attribute Float32Array? position;
-  [Constant, Throws] readonly attribute Float32Array? linearVelocity;
-  [Constant, Throws] readonly attribute Float32Array? linearAcceleration;
+interface VRPose : Pose
+{
 
-  /* orientation is a 4-entry array representing the components of a quaternion. */
-  [Constant, Throws] readonly attribute Float32Array? orientation;
-  /* angularVelocity and angularAcceleration are the components of 3-dimensional vectors. */
-  [Constant, Throws] readonly attribute Float32Array? angularVelocity;
-  [Constant, Throws] readonly attribute Float32Array? angularAcceleration;
 };
 
 [Constructor,
  Pref="dom.vr.enabled",
  HeaderFile="mozilla/dom/VRDisplay.h"]
 interface VRFrameData {
   readonly attribute DOMHighResTimeStamp timestamp;
 
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -362,16 +362,17 @@ WEBIDL_FILES = [
     'PeriodicWave.webidl',
     'Permissions.webidl',
     'PermissionSettings.webidl',
     'PermissionStatus.webidl',
     'Plugin.webidl',
     'PluginArray.webidl',
     'PointerEvent.webidl',
     'PopupBoxObject.webidl',
+    'Pose.webidl',
     'Position.webidl',
     'PositionError.webidl',
     'Presentation.webidl',
     'PresentationAvailability.webidl',
     'PresentationConnection.webidl',
     'PresentationConnectionList.webidl',
     'PresentationDeviceInfoManager.webidl',
     'PresentationReceiver.webidl',
@@ -642,16 +643,17 @@ if CONFIG['MOZ_WEBSPEECH']:
         'SpeechSynthesisEvent.webidl',
         'SpeechSynthesisUtterance.webidl',
         'SpeechSynthesisVoice.webidl',
     ]
 
 if CONFIG['MOZ_GAMEPAD']:
     WEBIDL_FILES += [
         'Gamepad.webidl',
+        'GamepadPose.webidl',
         'GamepadServiceTest.webidl'
     ]
 
 WEBIDL_FILES += [
     'CloseEvent.webidl',
     'CustomEvent.webidl',
     'DeviceOrientationEvent.webidl',
     'DeviceStorageChangeEvent.webidl',
--- a/editor/libeditor/HTMLEditRules.cpp
+++ b/editor/libeditor/HTMLEditRules.cpp
@@ -1951,16 +1951,29 @@ HTMLEditRules::WillDeleteSelection(Selec
                                              &so, address_of(visNode), &eo);
       NS_ENSURE_SUCCESS(rv, rv);
       NS_ENSURE_STATE(mHTMLEditor);
       *aHandled = true;
       rv = mHTMLEditor->DeleteText(nodeAsText, std::min(so, eo),
                                    DeprecatedAbs(eo - so));
       NS_ENSURE_SUCCESS(rv, rv);
 
+      // XXX When Backspace key is pressed, Chromium removes following empty
+      //     text nodes when removing the last character of the non-empty text
+      //     node.  However, Edge never removes empty text nodes even if
+      //     selection is in the following empty text node(s).  For now, we
+      //     should keep our traditional behavior same as Edge for backward
+      //     compatibility.
+      // XXX When Delete key is pressed, Edge removes all preceding empty
+      //     text nodes when removing the first character of the non-empty
+      //     text node.  Chromium removes only selected empty text node and
+      //     following empty text nodes and the first character of the
+      //     non-empty text node.  For now, we should keep our traditional
+      //     behavior same as Chromium for backward compatibility.
+
       DeleteNodeIfCollapsedText(nodeAsText);
 
       rv = InsertBRIfNeeded(aSelection);
       NS_ENSURE_SUCCESS(rv, rv);
 
       // Remember that we did a ranged delete for the benefit of
       // AfterEditInner().
       mDidRangedDelete = true;
--- a/editor/libeditor/WSRunObject.cpp
+++ b/editor/libeditor/WSRunObject.cpp
@@ -478,24 +478,22 @@ WSRunObject::PriorVisibleNode(nsINode* a
 
   WSFragment* run;
   FindRun(aNode, aOffset, &run, false);
 
   // Is there a visible run there or earlier?
   for (; run; run = run->mLeft) {
     if (run->mType == WSType::normalWS) {
       WSPoint point = GetCharBefore(aNode, aOffset);
-      if (point.mTextNode) {
+      // When it's a non-empty text node, return it.
+      if (point.mTextNode && point.mTextNode->Length()) {
         *outVisNode = point.mTextNode;
         *outVisOffset = point.mOffset + 1;
         if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) {
           *outType = WSType::normalWS;
-        } else if (!point.mChar) {
-          // MOOSE: not possible?
-          *outType = WSType::none;
         } else {
           *outType = WSType::text;
         }
         return;
       }
       // If no text node, keep looking.  We should eventually fall out of loop
     }
   }
@@ -522,24 +520,22 @@ WSRunObject::NextVisibleNode(nsINode* aN
 
   WSFragment* run;
   FindRun(aNode, aOffset, &run, true);
 
   // Is there a visible run there or later?
   for (; run; run = run->mRight) {
     if (run->mType == WSType::normalWS) {
       WSPoint point = GetCharAfter(aNode, aOffset);
-      if (point.mTextNode) {
+      // When it's a non-empty text node, return it.
+      if (point.mTextNode && point.mTextNode->Length()) {
         *outVisNode = point.mTextNode;
         *outVisOffset = point.mOffset;
         if (nsCRT::IsAsciiSpace(point.mChar) || point.mChar == nbsp) {
           *outType = WSType::normalWS;
-        } else if (!point.mChar) {
-          // MOOSE: not possible?
-          *outType = WSType::none;
         } else {
           *outType = WSType::text;
         }
         return;
       }
       // If no text node, keep looking.  We should eventually fall out of loop
     }
   }
--- a/editor/libeditor/tests/mochitest.ini
+++ b/editor/libeditor/tests/mochitest.ini
@@ -205,16 +205,17 @@ skip-if = os == 'android'
 subsuite = clipboard
 skip-if = toolkit == 'android'
 [test_bug1248128.html]
 [test_bug1250010.html]
 [test_bug1257363.html]
 [test_bug1248185.html]
 [test_bug1258085.html]
 [test_bug1268736.html]
+[test_bug1315065.html]
 
 [test_CF_HTML_clipboard.html]
 subsuite = clipboard
 [test_composition_event_created_in_chrome.html]
 [test_contenteditable_focus.html]
 [test_dom_input_event_on_htmleditor.html]
 skip-if = toolkit == 'android' # bug 1054087
 [test_dom_input_event_on_texteditor.html]
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_bug1315065.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1315065
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1315065</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315065">Mozilla Bug 1315065</a>
+<div contenteditable><p>abc<br></p></div>
+<script type="application/javascript">
+/** Test for Bug 1315065 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(()=>{
+  var editor = document.getElementsByTagName("div")[0];
+  function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) {
+    editor.innerHTML = "<p id='p'>abc<br></p>";
+    var p = document.getElementById("p");
+    // FYI: We cannot inserting empty text nodes as expected with
+    //      Node.appendChild() nor Node.insertBefore(). Therefore, let's use
+    //      Range.insertNode() like actual web apps.
+    var selection = window.getSelection();
+    selection.collapse(p, 1);
+    var range = selection.getRangeAt(0);
+    var emptyTextNode3 = document.createTextNode("");
+    range.insertNode(emptyTextNode3);
+    var emptyTextNode2 = document.createTextNode("");
+    range.insertNode(emptyTextNode2);
+    var emptyTextNode1 = document.createTextNode("");
+    range.insertNode(emptyTextNode1);
+    is(p.childNodes.length, 5, "Failed to initialize the editor");
+    is(p.childNodes.item(1), emptyTextNode1, "1st text node should be emptyTextNode1");
+    is(p.childNodes.item(2), emptyTextNode2, "2nd text node should be emptyTextNode2");
+    is(p.childNodes.item(3), emptyTextNode3, "3rd text node should be emptyTextNode3");
+    switch (aSelectionCollapsedTo) {
+      case 0:
+        selection.collapse(p.firstChild, 3); // next to 'c'
+        break;
+      case 1:
+        selection.collapse(emptyTextNode1, 0);
+        break;
+      case 2:
+        selection.collapse(emptyTextNode2, 0);
+        break;
+      case 3:
+        selection.collapse(emptyTextNode3, 0);
+        break;
+      default:
+        ok(false, "aSelectionCollapsedTo is illegal value");
+    }
+  }
+
+  for (var i = 0; i < 4; i++) {
+    const kDescription = i == 0 ? "Backspace from immediately after the last character" :
+                                  "Backspace from " + i + "th empty text node";
+    editor.focus();
+    initForBackspace(i);
+    synthesizeKey("KEY_Backspace", { code: "Backspace" });
+    var p = document.getElementById("p");
+    ok(p, kDescription + ": <p> element shouldn't be removed by Backspace key press");
+    is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Backspace key press");
+    // When Backspace key is pressed even in empty text nodes, Gecko should not remove empty text nodes for now
+    // because we should keep our traditional behavior (same as Edge) for backward compatibility as far as possible.
+    // In this case, Chromium removes all empty text nodes, but Edge doesn't remove any empty text nodes.
+    is(p.childNodes.length, 5, kDescription + ": <p> should have 5 children after pressing Backspace key");
+    is(p.childNodes.item(0).textContent, "ab", kDescription + ": 'c' should be removed by pressing Backspace key");
+    is(p.childNodes.item(1).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Backspace key");
+    is(p.childNodes.item(2).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Backspace key");
+    is(p.childNodes.item(3).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Backspace key");
+    editor.blur();
+  }
+
+  function initForDelete(aSelectionCollapsedTo /* = 0 ~ 3 */) {
+    editor.innerHTML = "<p id='p'>abc<br></p>";
+    var p = document.getElementById("p");
+    // FYI: We cannot inserting empty text nodes as expected with
+    //      Node.appendChild() nor Node.insertBefore(). Therefore, let's use
+    //      Range.insertNode() like actual web apps.
+    var selection = window.getSelection();
+    selection.collapse(p, 0);
+    var range = selection.getRangeAt(0);
+    var emptyTextNode1 = document.createTextNode("");
+    range.insertNode(emptyTextNode1);
+    var emptyTextNode2 = document.createTextNode("");
+    range.insertNode(emptyTextNode2);
+    var emptyTextNode3 = document.createTextNode("");
+    range.insertNode(emptyTextNode3);
+    is(p.childNodes.length, 5, "Failed to initialize the editor");
+    is(p.childNodes.item(0), emptyTextNode3, "1st text node should be emptyTextNode3");
+    is(p.childNodes.item(1), emptyTextNode2, "2nd text node should be emptyTextNode2");
+    is(p.childNodes.item(2), emptyTextNode1, "3rd text node should be emptyTextNode1");
+    switch (aSelectionCollapsedTo) {
+      case 0:
+        selection.collapse(p.childNodes.item(3), 0); // next to 'a'
+        break;
+      case 1:
+        selection.collapse(emptyTextNode1, 0);
+        break;
+      case 2:
+        selection.collapse(emptyTextNode2, 0);
+        break;
+      case 3:
+        selection.collapse(emptyTextNode3, 0);
+        break;
+      default:
+        ok(false, "aSelectionCollapsedTo is illegal value");
+    }
+  }
+
+  for (var i = 0; i < 4; i++) {
+    const kDescription = i == 0 ? "Delete from immediately before the first character" :
+                                  "Delete from " + i + "th empty text node";
+    editor.focus();
+    initForDelete(i);
+    synthesizeKey("KEY_Delete", { code: "Delete" });
+    var p = document.getElementById("p");
+    ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press");
+    is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Delete key press");
+    if (i == 0) {
+      // If Delete key is pressed in non-empty text node, only the text node should be modified.
+      // This is same behavior as Chromium, but different from Edge.  Edge removes all empty text nodes in this case.
+      is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed");
+      is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key");
+      is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key");
+      is(p.childNodes.item(2).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Delete key");
+      is(p.childNodes.item(3).textContent, "bc", kDescription + ": 'a' should be removed by pressing Delete key");
+    } else {
+      // If Delete key is pressed in an empty text node, it and following empty text nodes should be removed and the non-empty text node should be modified.
+      // This is same behavior as Chromium, but different from Edge.  Edge removes all empty text nodes in this case.
+      var expectedEmptyTextNodes = 3 - i;
+      is(p.childNodes.length, expectedEmptyTextNodes + 2, kDescription + ": <p> should have only " + i + " children after pressing Delete key (" + i + " empty text nodes should be removed");
+      is(p.childNodes.item(expectedEmptyTextNodes).textContent, "bc", kDescription + ": empty text nodes and 'a' should be removed by pressing Delete key");
+    }
+    editor.blur();
+  }
+  SimpleTest.finish();
+});
+</script>
+</body>
+</html>
--- a/gfx/vr/VRDisplayHost.cpp
+++ b/gfx/vr/VRDisplayHost.cpp
@@ -181,8 +181,21 @@ VRControllerHost::SetButtonPressed(uint6
   mButtonPressed = aBit;
 }
 
 uint64_t
 VRControllerHost::GetButtonPressed()
 {
   return mButtonPressed;
 }
+
+void
+VRControllerHost::SetPose(const dom::GamepadPoseState& aPose)
+{
+  mPose = aPose;
+}
+
+const dom::GamepadPoseState&
+VRControllerHost::GetPose()
+{
+  return mPose;
+}
+
--- a/gfx/vr/VRDisplayHost.h
+++ b/gfx/vr/VRDisplayHost.h
@@ -11,16 +11,17 @@
 #include "nsString.h"
 #include "nsCOMPtr.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/EnumeratedArray.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/TypedEnumBits.h"
+#include "mozilla/dom/GamepadPoseState.h"
 
 namespace mozilla {
 namespace layers {
 class PTextureParent;
 #if defined(XP_WIN)
 class TextureSourceD3D11;
 #endif
 } // namespace layers
@@ -87,24 +88,27 @@ class VRControllerHost {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VRControllerHost)
 
   const VRControllerInfo& GetControllerInfo() const;
   void SetIndex(uint32_t aIndex);
   uint32_t GetIndex();
   void SetButtonPressed(uint64_t aBit);
   uint64_t GetButtonPressed();
+  void SetPose(const dom::GamepadPoseState& aPose);
+  const dom::GamepadPoseState& GetPose();
 
 protected:
   explicit VRControllerHost(VRDeviceType aType);
   virtual ~VRControllerHost();
 
   VRControllerInfo mControllerInfo;
   // The controller index in VRControllerManager.
   uint32_t mIndex;
   // The current button pressed bit of button mask.
   uint64_t mButtonPressed;
+  dom::GamepadPoseState mPose;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif /* GFX_VR_DISPLAY_HOST_H */
--- a/gfx/vr/VRManager.cpp
+++ b/gfx/vr/VRManager.cpp
@@ -89,16 +89,20 @@ VRManager::VRManager()
   }
 
   // OSVR is cross platform compatible
   mgr = VRDisplayManagerOSVR::Create();
   if (mgr) {
       mManagers.AppendElement(mgr);
   }
 #endif
+  // Enable gamepad extensions while VR is enabled.
+  if (gfxPrefs::VREnabled()) {
+    Preferences::SetBool("dom.gamepad.extensions.enabled", true);
+  }
 }
 
 VRManager::~VRManager()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mInitialized);
   MOZ_COUNT_DTOR(VRManager);
 }
--- a/gfx/vr/gfxVR.cpp
+++ b/gfx/vr/gfxVR.cpp
@@ -108,8 +108,20 @@ VRControllerManager::NewAxisMove(uint32_
 {
   dom::GamepadAxisInformation a(aIndex, dom::GamepadServiceType::VR,
                                 aAxis, aValue);
 
   VRManager* vm = VRManager::Get();
   MOZ_ASSERT(vm);
   vm->NotifyGamepadChange<dom::GamepadAxisInformation>(a);
 }
+
+void
+VRControllerManager::NewPoseState(uint32_t aIndex,
+                                  const dom::GamepadPoseState& aPose)
+{
+  dom::GamepadPoseInformation a(aIndex, dom::GamepadServiceType::VR,
+                                aPose);
+
+  VRManager* vm = VRManager::Get();
+  MOZ_ASSERT(vm);
+  vm->NotifyGamepadChange<dom::GamepadPoseInformation>(a);
+}
--- a/gfx/vr/gfxVR.h
+++ b/gfx/vr/gfxVR.h
@@ -15,16 +15,20 @@
 #include "mozilla/EnumeratedArray.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/TypedEnumBits.h"
 
 namespace mozilla {
 namespace layers {
 class PTextureParent;
 }
+namespace dom {
+enum class GamepadMappingType : uint32_t;
+struct GamepadPoseState;
+}
 namespace gfx {
 class VRLayerParent;
 class VRDisplayHost;
 class VRControllerHost;
 
 enum class VRDeviceType : uint16_t {
   Oculus,
   OpenVR,
@@ -247,16 +251,17 @@ public:
   static uint32_t AllocateControllerID();
   virtual bool Init() = 0;
   virtual void Destroy() = 0;
   virtual void HandleInput() = 0;
   virtual void GetControllers(nsTArray<RefPtr<VRControllerHost>>& aControllerResult) = 0;
   virtual void ScanForDevices() = 0;
   void NewButtonEvent(uint32_t aIndex, uint32_t aButton, bool aPressed);
   void NewAxisMove(uint32_t aIndex, uint32_t aAxis, double aValue);
+  void NewPoseState(uint32_t aIndex, const dom::GamepadPoseState& aPose);
   void AddGamepad(const char* aID, uint32_t aMapping,
                   uint32_t aNumButtons, uint32_t aNumAxes);
   void RemoveGamepad(uint32_t aIndex);
 
 protected:
   VRControllerManager() : mInstalled(false), mControllerCount(0) {}
   virtual ~VRControllerManager() {}
 
@@ -264,14 +269,17 @@ protected:
   uint32_t mControllerCount;
   static Atomic<uint32_t> sControllerBase;
 
 private:
   virtual void HandleButtonPress(uint32_t aControllerIdx,
                                  uint64_t aButtonPressed) = 0;
   virtual void HandleAxisMove(uint32_t aControllerIdx, uint32_t aAxis,
                               float aValue) = 0;
+  virtual void HandlePoseTracking(uint32_t aControllerIdx,
+                                  const dom::GamepadPoseState& aPose,
+                                  VRControllerHost* aController) = 0;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif /* GFX_VR_H */
--- a/gfx/vr/gfxVROpenVR.cpp
+++ b/gfx/vr/gfxVROpenVR.cpp
@@ -485,17 +485,17 @@ VRDisplayManagerOpenVR::GetHMDs(nsTArray
 }
 
 VRControllerOpenVR::VRControllerOpenVR()
   : VRControllerHost(VRDeviceType::OpenVR)
 {
   MOZ_COUNT_CTOR_INHERITED(VRControllerOpenVR, VRControllerHost);
   mControllerInfo.mControllerName.AssignLiteral("OpenVR HMD");
 #ifdef MOZ_GAMEPAD
-  mControllerInfo.mMappingType = static_cast<uint32_t>(dom::GamepadMappingType::_empty);
+  mControllerInfo.mMappingType = static_cast<uint32_t>(GamepadMappingType::_empty);
 #else
   mControllerInfo.mMappingType = 0;
 #endif
   mControllerInfo.mNumButtons = gNumOpenVRButtonMask;
   mControllerInfo.mNumAxes = gNumOpenVRAxis;
 }
 
 VRControllerOpenVR::~VRControllerOpenVR()
@@ -578,16 +578,19 @@ VRControllerManagerOpenVR::HandleInput()
   uint32_t axis = 0;
 
   if (!mOpenVRInstalled) {
     return;
   }
 
   MOZ_ASSERT(mVRSystem);
 
+  vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
+  mVRSystem->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseSeated, 0.0f,
+                                             poses, vr::k_unMaxTrackedDeviceCount);
   // Process OpenVR controller state
   for (uint32_t i = 0; i < mOpenVRController.Length(); ++i) {
     controller = mOpenVRController[i];
 
     MOZ_ASSERT(mVRSystem->GetTrackedDeviceClass(controller->GetTrackedIndex())
                == vr::TrackedDeviceClass_Controller);
 
     if (mVRSystem->GetControllerState(controller->GetTrackedIndex(), &state)) {
@@ -600,16 +603,54 @@ VRControllerManagerOpenVR::HandleInput()
       axis = static_cast<uint32_t>(VRControllerAxisType::TrackpadYAxis);
       HandleAxisMove(controller->GetIndex(), axis,
                      state.rAxis[gOpenVRAxes[axis]].y);
 
       axis = static_cast<uint32_t>(VRControllerAxisType::Trigger);
       HandleAxisMove(controller->GetIndex(), axis,
                      state.rAxis[gOpenVRAxes[axis]].x);
     }
+
+    // Start to process pose
+    const ::vr::TrackedDevicePose_t& pose = poses[controller->GetTrackedIndex()];
+
+    if (pose.bDeviceIsConnected && pose.bPoseIsValid &&
+      pose.eTrackingResult == vr::TrackingResult_Running_OK) {
+      gfx::Matrix4x4 m;
+
+      // NOTE! mDeviceToAbsoluteTracking is a 3x4 matrix, not 4x4.  But
+      // because of its arrangement, we can copy the 12 elements in and
+      // then transpose them to the right place.  We do this so we can
+      // pull out a Quaternion.
+      memcpy(&m.components, &pose.mDeviceToAbsoluteTracking, sizeof(float) * 12);
+      m.Transpose();
+
+      gfx::Quaternion rot;
+      rot.SetFromRotationMatrix(m);
+      rot.Invert();
+
+      GamepadPoseState poseState;
+      poseState.flags |= GamepadCapabilityFlags::Cap_Orientation;
+      poseState.orientation[0] = rot.x;
+      poseState.orientation[1] = rot.y;
+      poseState.orientation[2] = rot.z;
+      poseState.orientation[3] = rot.w;
+      poseState.angularVelocity[0] = pose.vAngularVelocity.v[0];
+      poseState.angularVelocity[1] = pose.vAngularVelocity.v[1];
+      poseState.angularVelocity[2] = pose.vAngularVelocity.v[2];
+
+      poseState.flags |= GamepadCapabilityFlags::Cap_Position;
+      poseState.position[0] = m._41;
+      poseState.position[1] = m._42;
+      poseState.position[2] = m._43;
+      poseState.linearVelocity[0] = pose.vVelocity.v[0];
+      poseState.linearVelocity[1] = pose.vVelocity.v[1];
+      poseState.linearVelocity[2] = pose.vVelocity.v[2];
+      HandlePoseTracking(controller->GetIndex(), poseState, controller);
+    }
   }
 }
 
 void
 VRControllerManagerOpenVR::HandleButtonPress(uint32_t aControllerIdx,
                                              uint64_t aButtonPressed)
 {
   uint64_t buttonMask = 0;
@@ -640,16 +681,27 @@ VRControllerManagerOpenVR::HandleAxisMov
                                           float aValue)
 {
   if (aValue != 0.0f) {
     NewAxisMove(aControllerIdx, aAxis, aValue);
   }
 }
 
 void
+VRControllerManagerOpenVR::HandlePoseTracking(uint32_t aControllerIdx,
+                                              const GamepadPoseState& aPose,
+                                              VRControllerHost* aController)
+{
+  if (aPose != aController->GetPose()) {
+    aController->SetPose(aPose);
+    NewPoseState(aControllerIdx, aPose);
+  }
+}
+
+void
 VRControllerManagerOpenVR::GetControllers(nsTArray<RefPtr<VRControllerHost>>& aControllerResult)
 {
   if (!mOpenVRInstalled) {
     return;
   }
 
   aControllerResult.Clear();
   for (uint32_t i = 0; i < mOpenVRController.Length(); ++i) {
--- a/gfx/vr/gfxVROpenVR.h
+++ b/gfx/vr/gfxVROpenVR.h
@@ -119,16 +119,19 @@ public:
 private:
   VRControllerManagerOpenVR();
   ~VRControllerManagerOpenVR();
 
   virtual void HandleButtonPress(uint32_t aControllerIdx,
                                  uint64_t aButtonPressed) override;
   virtual void HandleAxisMove(uint32_t aControllerIdx, uint32_t aAxis,
                               float aValue) override;
+  virtual void HandlePoseTracking(uint32_t aControllerIdx,
+                                  const dom::GamepadPoseState& aPose,
+                                  VRControllerHost* aController) override;
 
   bool mOpenVRInstalled;
   nsTArray<RefPtr<impl::VRControllerOpenVR>> mOpenVRController;
   vr::IVRSystem *mVRSystem;
 };
 
 } // namespace gfx
 } // namespace mozilla
--- a/layout/style/nsCSSPropList.h
+++ b/layout/style/nsCSSPropList.h
@@ -464,27 +464,27 @@ CSS_PROP_DISPLAY(
     -moz-appearance,
     appearance,
     CSS_PROP_DOMPROP_PREFIXED(Appearance),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kAppearanceKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     backface-visibility,
     backface_visibility,
     BackfaceVisibility,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBackfaceVisibilityKTable,
     offsetof(nsStyleDisplay, mBackfaceVisibility),
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     background,
     background,
     Background,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_BACKGROUND(
     background-attachment,
@@ -493,43 +493,43 @@ CSS_PROP_BACKGROUND(
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerAttachmentKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BACKGROUND(
     background-blend-mode,
     background_blend_mode,
     BackgroundBlendMode,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "layout.css.background-blend-mode.enabled",
     VARIANT_KEYWORD, // used by list parsing
     kBlendModeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BACKGROUND(
     background-clip,
     background_clip,
     BackgroundClip,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kBackgroundClipKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BACKGROUND(
     background-color,
     background_color,
     BackgroundColor,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED |
@@ -548,30 +548,30 @@ CSS_PROP_BACKGROUND(
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED |
         CSS_PROPERTY_START_IMAGE_LOADS,
     "",
     VARIANT_IMAGE, // used by list parsing
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BACKGROUND(
     background-origin,
     background_origin,
     BackgroundOrigin,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerOriginKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     background-position,
     background_position,
     BackgroundPosition,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK,
     "")
 CSS_PROP_BACKGROUND(
@@ -609,17 +609,17 @@ CSS_PROP_BACKGROUND(
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerRepeatKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BACKGROUND(
     background-size,
     background_size,
     BackgroundSize,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
@@ -798,17 +798,17 @@ CSS_PROP_BORDER(
     CSS_PROP_DOMPROP_PREFIXED(BorderBottomColors),
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-bottom-left-radius,
     border_bottom_left_radius,
     BorderBottomLeftRadius,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_STORES_CALC,
@@ -835,17 +835,17 @@ CSS_PROP_BORDER(
     border_bottom_style,
     BorderBottomStyle,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     VARIANT_HK,
     kBorderStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)  // on/off will need reflow
+    eStyleAnimType_Discrete)  // on/off will need reflow
 CSS_PROP_BORDER(
     border-bottom-width,
     border_bottom_width,
     BorderBottomWidth,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
@@ -859,17 +859,17 @@ CSS_PROP_TABLEBORDER(
     border-collapse,
     border_collapse,
     BorderCollapse,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBorderCollapseKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     border-color,
     border_color,
     BorderColor,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_HASHLESS_COLOR_QUIRK,
     "")
 CSS_PROP_SHORTHAND(
@@ -883,62 +883,62 @@ CSS_PROP_BORDER(
     border_image_outset,
     BorderImageOutset,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-image-repeat,
     border_image_repeat,
     BorderImageRepeat,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     0,
     kBorderImageRepeatKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-image-slice,
     border_image_slice,
     BorderImageSlice,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     0,
     kBorderImageSliceKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-image-source,
     border_image_source,
     BorderImageSource,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_START_IMAGE_LOADS,
     "",
     VARIANT_IMAGE | VARIANT_INHERIT,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-image-width,
     border_image_width,
     BorderImageWidth,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     border-inline-end,
     border_inline_end,
     BorderInlineEnd,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_LOGICAL(
     border-inline-end-color,
@@ -1062,28 +1062,28 @@ CSS_PROP_BORDER(
     CSS_PROP_DOMPROP_PREFIXED(BorderLeftColors),
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-left-style,
     border_left_style,
     BorderLeftStyle,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     VARIANT_HK,
     kBorderStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-left-width,
     border_left_width,
     BorderLeftWidth,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH |
@@ -1124,28 +1124,28 @@ CSS_PROP_BORDER(
     CSS_PROP_DOMPROP_PREFIXED(BorderRightColors),
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-right-style,
     border_right_style,
     BorderRightStyle,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     VARIANT_HK,
     kBorderStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-right-width,
     border_right_width,
     BorderRightWidth,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH |
@@ -1198,17 +1198,17 @@ CSS_PROP_BORDER(
     CSS_PROP_DOMPROP_PREFIXED(BorderTopColors),
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     border-top-left-radius,
     border_top_left_radius,
     BorderTopLeftRadius,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_STORES_CALC,
@@ -1235,17 +1235,17 @@ CSS_PROP_BORDER(
     border_top_style,
     BorderTopStyle,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     VARIANT_HK,
     kBorderStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)  // on/off will need reflow
+    eStyleAnimType_Discrete)  // on/off will need reflow
 CSS_PROP_BORDER(
     border-top-width,
     border_top_width,
     BorderTopWidth,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
@@ -1279,37 +1279,37 @@ CSS_PROP_XUL(
     -moz-box-align,
     box_align,
     CSS_PROP_DOMPROP_PREFIXED(BoxAlign),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBoxAlignKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_BORDER(
     box-decoration-break,
     box_decoration_break,
     BoxDecorationBreak,
     CSS_PROPERTY_PARSE_VALUE,
     "layout.css.box-decoration-break.enabled",
     VARIANT_HK,
     kBoxDecorationBreakKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_XUL(
     -moz-box-direction,
     box_direction,
     CSS_PROP_DOMPROP_PREFIXED(BoxDirection),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBoxDirectionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_XUL(
     -moz-box-flex,
     box_flex,
     CSS_PROP_DOMPROP_PREFIXED(BoxFlex),
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE,
     "",
     VARIANT_HN,
@@ -1321,37 +1321,37 @@ CSS_PROP_XUL(
     box_ordinal_group,
     CSS_PROP_DOMPROP_PREFIXED(BoxOrdinalGroup),
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE,
     "",
     VARIANT_HI,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_XUL(
     -moz-box-orient,
     box_orient,
     CSS_PROP_DOMPROP_PREFIXED(BoxOrient),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBoxOrientKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_XUL(
     -moz-box-pack,
     box_pack,
     CSS_PROP_DOMPROP_PREFIXED(BoxPack),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBoxPackKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_EFFECTS(
     box-shadow,
     box_shadow,
     BoxShadow,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED,
@@ -1365,37 +1365,37 @@ CSS_PROP_POSITION(
     box-sizing,
     box_sizing,
     BoxSizing,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBoxSizingKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TABLEBORDER(
     caption-side,
     caption_side,
     CaptionSide,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kCaptionSideKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     clear,
     clear,
     Clear,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kClearKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_EFFECTS(
     clip,
     clip,
     Clip,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK,
     "",
     0,
@@ -1443,17 +1443,17 @@ CSS_PROP_VISIBILITY(
     color-adjust,
     color_adjust,
     ColorAdjust,
     CSS_PROPERTY_PARSE_VALUE,
     "layout.css.color-adjust.enabled",
     VARIANT_HK,
     kColorAdjustKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVG(
     color-interpolation,
     color_interpolation,
     ColorInterpolation,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kColorInterpolationKTable,
@@ -1486,17 +1486,17 @@ CSS_PROP_COLUMN(
     column-fill,
     column_fill,
     ColumnFill,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kColumnFillKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_COLUMN(
     column-gap,
     column_gap,
     ColumnGap,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE,
     "",
     VARIANT_HL | VARIANT_NORMAL | VARIANT_CALC,
@@ -1524,17 +1524,17 @@ CSS_PROP_COLUMN(
     column-rule-style,
     column_rule_style,
     ColumnRuleStyle,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kBorderStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_COLUMN(
     column-rule-width,
     column_rule_width,
     ColumnRuleWidth,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE,
     "",
     VARIANT_HKL | VARIANT_CALC,
@@ -1565,28 +1565,28 @@ CSS_PROP_DISPLAY(
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_FIXPOS_CB,
     "layout.css.contain.enabled",
     // Does not affect parsing, but is needed for tab completion in devtools:
     VARIANT_HK | VARIANT_NONE,
     kContainKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_CONTENT(
     content,
     content,
     Content,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_START_IMAGE_LOADS,
     "",
     0,
     kContentKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #ifndef CSS_PROP_LIST_EXCLUDE_INTERNAL
 CSS_PROP_TEXT(
     -moz-control-character-visibility,
     _moz_control_character_visibility,
     CSS_PROP_DOMPROP_PREFIXED(ControlCharacterVisibility),
     CSS_PROPERTY_INTERNAL |
         CSS_PROPERTY_PARSE_VALUE,
     "",
@@ -1599,51 +1599,51 @@ CSS_PROP_CONTENT(
     counter-increment,
     counter_increment,
     CounterIncrement,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 137285
+    eStyleAnimType_Discrete) // XXX bug 137285
 CSS_PROP_CONTENT(
     counter-reset,
     counter_reset,
     CounterReset,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 137285
+    eStyleAnimType_Discrete) // XXX bug 137285
 CSS_PROP_USERINTERFACE(
     cursor,
     cursor,
     Cursor,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
         CSS_PROPERTY_START_IMAGE_LOADS |
         CSS_PROPERTY_IMAGE_IS_IN_ARRAY_0,
     "",
     0,
     kCursorKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #ifndef CSS_PROP_LIST_ONLY_COMPONENTS_OF_ALL_SHORTHAND
 CSS_PROP_VISIBILITY(
     direction,
     direction,
     Direction,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kDirectionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #endif // !defined(CSS_PROP_LIST_ONLY_COMPONENTS_OF_ALL_SHORTHAND)
 CSS_PROP_DISPLAY(
     display,
     display,
     Display,
     CSS_PROPERTY_PARSE_VALUE |
         // This is allowed because we need to make the placeholder
         // pseudo-element an inline-block in the UA stylesheet. It is a block
@@ -1668,17 +1668,17 @@ CSS_PROP_TABLEBORDER(
     empty-cells,
     empty_cells,
     EmptyCells,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kEmptyCellsKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVG(
     fill,
     fill,
     Fill,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     kContextPatternKTable,
@@ -1796,27 +1796,27 @@ CSS_PROP_DISPLAY(
     float_,
     CSS_PROP_PUBLIC_OR_PRIVATE(CssFloat, Float),
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "",
     VARIANT_HK,
     kFloatKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_BORDER(
     -moz-float-edge,
     float_edge,
     CSS_PROP_DOMPROP_PREFIXED(FloatEdge),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kFloatEdgeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_SVGRESET(
     flood-color,
     flood_color,
     FloodColor,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HC,
     nullptr,
@@ -1845,54 +1845,54 @@ CSS_PROP_FONT(
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-feature-settings,
     font_feature_settings,
     FontFeatureSettings,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-kerning,
     font_kerning,
     FontKerning,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HK,
     kFontKerningKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-language-override,
     font_language_override,
     FontLanguageOverride,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_NORMAL | VARIANT_INHERIT | VARIANT_STRING,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-size,
     font_size,
     FontSize,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
@@ -1948,17 +1948,17 @@ CSS_PROP_FONT(
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     kFontSynthesisKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     font-variant,
     font_variant,
     FontVariant,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_FONT(
     font-variant-alternates,
@@ -1967,80 +1967,80 @@ CSS_PROP_FONT(
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     kFontVariantAlternatesKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-variant-caps,
     font_variant_caps,
     FontVariantCaps,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HMK,
     kFontVariantCapsKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-variant-east-asian,
     font_variant_east_asian,
     FontVariantEastAsian,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     kFontVariantEastAsianKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-variant-ligatures,
     font_variant_ligatures,
     FontVariantLigatures,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     kFontVariantLigaturesKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-variant-numeric,
     font_variant_numeric,
     FontVariantNumeric,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     kFontVariantNumericKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-variant-position,
     font_variant_position,
     FontVariantPosition,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HMK,
     kFontVariantPositionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     font-weight,
     font_weight,
     FontWeight,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
@@ -2055,17 +2055,17 @@ CSS_PROP_UIRESET(
     force_broken_image_icon,
     CSS_PROP_DOMPROP_PREFIXED(ForceBrokenImageIcon),
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE,
     "",
     VARIANT_HI,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // bug 58646
+    eStyleAnimType_Discrete) // bug 58646
 CSS_PROP_SHORTHAND(
     grid,
     grid,
     Grid,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled")
 CSS_PROP_SHORTHAND(
     grid-area,
@@ -2079,56 +2079,56 @@ CSS_PROP_POSITION(
     GridAutoColumns,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
     0,
     kGridTrackBreadthKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     grid-auto-flow,
     grid_auto_flow,
     GridAutoFlow,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
     0,
     kGridAutoFlowKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     grid-auto-rows,
     grid_auto_rows,
     GridAutoRows,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
     0,
     kGridTrackBreadthKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     grid-column,
     grid_column,
     GridColumn,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled")
 CSS_PROP_POSITION(
     grid-column-end,
     grid_column_end,
     GridColumnEnd,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     grid-column-gap,
     grid_column_gap,
     GridColumnGap,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
@@ -2140,17 +2140,17 @@ CSS_PROP_POSITION(
     grid-column-start,
     grid_column_start,
     GridColumnStart,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     grid-gap,
     grid_gap,
     GridGap,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled")
 CSS_PROP_SHORTHAND(
     grid-row,
@@ -2162,17 +2162,17 @@ CSS_PROP_POSITION(
     grid-row-end,
     grid_row_end,
     GridRowEnd,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     grid-row-gap,
     grid_row_gap,
     GridRowGap,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
@@ -2184,60 +2184,60 @@ CSS_PROP_POSITION(
     grid-row-start,
     grid_row_start,
     GridRowStart,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     grid-template,
     grid_template,
     GridTemplate,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.grid.enabled")
 CSS_PROP_POSITION(
     grid-template-areas,
     grid_template_areas,
     GridTemplateAreas,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     grid-template-columns,
     grid_template_columns,
     GridTemplateColumns,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
     0,
     kGridTrackBreadthKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     grid-template-rows,
     grid_template_rows,
     GridTemplateRows,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.grid.enabled",
     0,
     kGridTrackBreadthKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     height,
     height,
     Height,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
@@ -2251,39 +2251,39 @@ CSS_PROP_TEXT(
     hyphens,
     hyphens,
     Hyphens,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kHyphensKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TEXTRESET(
     initial-letter,
     initial_letter,
     InitialLetter,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "layout.css.initial-letter.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_VISIBILITY(
     image-orientation,
     image_orientation,
     ImageOrientation,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION,
     "layout.css.image-orientation.enabled",
     0,
     kImageOrientationKTable,
     offsetof(nsStyleVisibility, mImageOrientation),
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_LIST(
     -moz-image-region,
     image_region,
     CSS_PROP_DOMPROP_PREFIXED(ImageRegion),
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr,
@@ -2303,17 +2303,17 @@ CSS_PROP_UIRESET(
     ime-mode,
     ime_mode,
     ImeMode,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kIMEModeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_LOGICAL(
     inline-size,
     inline_size,
     InlineSize,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH |
@@ -2331,17 +2331,17 @@ CSS_PROP_DISPLAY(
     isolation,
     Isolation,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_CREATES_STACKING_CONTEXT,
     "layout.css.isolation.enabled",
     VARIANT_HK,
     kIsolationKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     justify-content,
     justify_content,
     JustifyContent,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     VARIANT_HK,
     kAutoCompletionAlignJustifyContent,
@@ -2444,38 +2444,38 @@ CSS_PROP_LIST(
     list_style_image,
     ListStyleImage,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_START_IMAGE_LOADS,
     "",
     VARIANT_HUO,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_LIST(
     list-style-position,
     list_style_position,
     ListStylePosition,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kListStylePositionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_LIST(
     list-style-type,
     list_style_type,
     ListStyleType,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     margin,
     margin,
     Margin,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
         CSS_PROPERTY_APPLIES_TO_PAGE_RULE,
     "")
@@ -2619,49 +2619,49 @@ CSS_PROP_SVG(
     marker-end,
     marker_end,
     MarkerEnd,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HUO,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVG(
     marker-mid,
     marker_mid,
     MarkerMid,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HUO,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVG(
     marker-start,
     marker_start,
     MarkerStart,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HUO,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #ifndef MOZ_ENABLE_MASK_AS_SHORTHAND
 CSS_PROP_SVGRESET(
     mask,
     mask,
     Mask,
     CSS_PROPERTY_PARSE_VALUE |
       CSS_PROPERTY_CREATES_STACKING_CONTEXT,
     "",
     VARIANT_HUO,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #else
 CSS_PROP_SHORTHAND(
     mask,
     mask,
     Mask,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_SVGRESET(
@@ -2669,63 +2669,63 @@ CSS_PROP_SVGRESET(
     mask_clip,
     MaskClip,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerOriginKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVGRESET(
     mask-composite,
     mask_composite,
     MaskComposite,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerCompositeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVGRESET(
     mask-image,
     mask_image,
     MaskImage,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_CREATES_STACKING_CONTEXT |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
         CSS_PROPERTY_START_IMAGE_LOADS,
     "",
     VARIANT_IMAGE, // used by list parsing
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVGRESET(
     mask-mode,
     mask_mode,
     MaskMode,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerModeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVGRESET(
     mask-origin,
     mask_origin,
     MaskOrigin,
     CSS_PROPERTY_PARSE_VALUE_LIST |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerOriginKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     mask-position,
     mask_position,
     MaskPosition,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK,
     "")
 CSS_PROP_SVGRESET(
@@ -2757,17 +2757,17 @@ CSS_PROP_SVGRESET(
     mask_repeat,
     MaskRepeat,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     VARIANT_KEYWORD, // used by list parsing
     kImageLayerRepeatKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVGRESET(
     mask-size,
     mask_size,
     MaskSize,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_STORES_CALC,
@@ -2955,28 +2955,28 @@ CSS_PROP_EFFECTS(
     mix_blend_mode,
     MixBlendMode,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_CREATES_STACKING_CONTEXT,
     "layout.css.mix-blend-mode.enabled",
     VARIANT_HK,
     kBlendModeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     object-fit,
     object_fit,
     ObjectFit,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.object-fit-and-position.enabled",
     VARIANT_HK,
     kObjectFitKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     object-position,
     object_position,
     ObjectPosition,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.object-fit-and-position.enabled",
@@ -3075,29 +3075,29 @@ CSS_PROP_DISPLAY(
     -moz-orient,
     orient,
     CSS_PROP_DOMPROP_PREFIXED(Orient),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kOrientKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_FONT(
     -moz-osx-font-smoothing,
     osx_font_smoothing,
     CSS_PROP_DOMPROP_PREFIXED(OsxFontSmoothing),
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "layout.css.osx-font-smoothing.enabled",
     VARIANT_HK,
     kFontSmoothingKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     outline,
     outline,
     Outline,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_OUTLINE(
     outline-color,
@@ -3178,17 +3178,17 @@ CSS_PROP_OUTLINE(
     outline-style,
     outline_style,
     OutlineStyle,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kOutlineStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_OUTLINE(
     outline-width,
     outline_width,
     OutlineWidth,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE,
     "",
     VARIANT_HKL | VARIANT_CALC,
@@ -3207,41 +3207,41 @@ CSS_PROP_DISPLAY(
     OverflowClipBox,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "layout.css.overflow-clip-box.enabled",
     VARIANT_HK,
     kOverflowClipBoxKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     overflow-x,
     overflow_x,
     OverflowX,
     CSS_PROPERTY_PARSE_VALUE |
         // This is required by the UA stylesheet and can't be overridden.
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HK,
     kOverflowSubKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     overflow-y,
     overflow_y,
     OverflowY,
     CSS_PROPERTY_PARSE_VALUE |
         // This is required by the UA stylesheet and can't be overridden.
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HK,
     kOverflowSubKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     padding,
     padding,
     Padding,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK,
     "")
 CSS_PROP_LOGICAL(
@@ -3396,47 +3396,47 @@ CSS_PROP_DISPLAY(
     page-break-after,
     page_break_after,
     PageBreakAfter,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kPageBreakKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // temp fix for bug 24000
+    eStyleAnimType_Discrete) // temp fix for bug 24000
 CSS_PROP_DISPLAY(
     page-break-before,
     page_break_before,
     PageBreakBefore,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kPageBreakKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // temp fix for bug 24000
+    eStyleAnimType_Discrete) // temp fix for bug 24000
 CSS_PROP_DISPLAY(
     page-break-inside,
     page_break_inside,
     PageBreakInside,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kPageBreakInsideKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVG(
     paint-order,
     paint_order,
     PaintOrder,
     CSS_PROPERTY_PARSE_FUNCTION,
     "svg.paint-order.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     perspective,
     perspective,
     Perspective,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_CREATES_STACKING_CONTEXT |
         CSS_PROPERTY_FIXPOS_CB,
     "",
@@ -3475,41 +3475,41 @@ CSS_PROP_DISPLAY(
     CSS_PROPERTY_PARSE_VALUE |
         // For position: sticky/fixed
         CSS_PROPERTY_CREATES_STACKING_CONTEXT |
         CSS_PROPERTY_ABSPOS_CB,
     "",
     VARIANT_HK,
     kPositionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_LIST(
     quotes,
     quotes,
     Quotes,
     CSS_PROPERTY_PARSE_FUNCTION,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     resize,
     resize,
     Resize,
     CSS_PROPERTY_PARSE_VALUE |
         // This is allowed because the UA stylesheet sets 'resize: both;' on
         // textarea and we need to disable this for the placeholder
         // pseudo-element.
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HK,
     kResizeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     right,
     right,
     Right,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH,
@@ -3586,104 +3586,104 @@ CSS_PROP_DISPLAY(
     scroll-behavior,
     scroll_behavior,
     ScrollBehavior,
     CSS_PROPERTY_PARSE_VALUE,
     "layout.css.scroll-behavior.property-enabled",
     VARIANT_HK,
     kScrollBehaviorKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     scroll-snap-coordinate,
     scroll_snap_coordinate,
     ScrollSnapCoordinate,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS |
         CSS_PROPERTY_STORES_CALC,
     "layout.css.scroll-snap.enabled",
     0,
     kImageLayerPositionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     scroll-snap-destination,
     scroll_snap_destination,
     ScrollSnapDestination,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_STORES_CALC,
     "layout.css.scroll-snap.enabled",
     0,
     kImageLayerPositionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     scroll-snap-points-x,
     scroll_snap_points_x,
     ScrollSnapPointsX,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_STORES_CALC,
     "layout.css.scroll-snap.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     scroll-snap-points-y,
     scroll_snap_points_y,
     ScrollSnapPointsY,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_STORES_CALC,
     "layout.css.scroll-snap.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     scroll-snap-type,
     scroll_snap_type,
     ScrollSnapType,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.scroll-snap.enabled")
 CSS_PROP_DISPLAY(
     scroll-snap-type-x,
     scroll_snap_type_x,
     ScrollSnapTypeX,
     CSS_PROPERTY_PARSE_VALUE,
     "layout.css.scroll-snap.enabled",
     VARIANT_HK,
     kScrollSnapTypeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     scroll-snap-type-y,
     scroll_snap_type_y,
     ScrollSnapTypeY,
     CSS_PROPERTY_PARSE_VALUE,
     "layout.css.scroll-snap.enabled",
     VARIANT_HK,
     kScrollSnapTypeKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     shape-outside,
     shape_outside,
     ShapeOutside,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER,
     "layout.css.shape-outside.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // FIXME: Bug 1289049 for adding animation support
+    eStyleAnimType_Discrete) // FIXME: Bug 1289049 for adding animation support
 CSS_PROP_SVG(
     shape-rendering,
     shape_rendering,
     ShapeRendering,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kShapeRenderingKTable,
@@ -3708,17 +3708,17 @@ CSS_PROP_XUL(
     -moz-stack-sizing,
     stack_sizing,
     CSS_PROP_DOMPROP_PREFIXED(StackSizing),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kStackSizingKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVGRESET(
     stop-color,
     stop_color,
     StopColor,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HC,
     nullptr,
@@ -3841,50 +3841,50 @@ CSS_PROP_TEXT(
     _moz_tab_size,
     CSS_PROP_DOMPROP_PREFIXED(TabSize),
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE,
     "",
     VARIANT_HI,
     nullptr,
     offsetof(nsStyleText, mTabSize),
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TABLE(
     table-layout,
     table_layout,
     TableLayout,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kTableLayoutKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TEXT(
     text-align,
     text_align,
     TextAlign,
     CSS_PROPERTY_PARSE_VALUE | CSS_PROPERTY_VALUE_PARSER_FUNCTION |
       CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     // When we support aligning on a string, we can parse text-align
     // as a string....
     VARIANT_HK /* | VARIANT_STRING */,
     kTextAlignKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TEXT(
     text-align-last,
     text_align_last,
     TextAlignLast,
     CSS_PROPERTY_PARSE_VALUE | CSS_PROPERTY_VALUE_PARSER_FUNCTION,
     "",
     VARIANT_HK,
     kTextAlignLastKTable,
     offsetof(nsStyleText, mTextAlignLast),
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SVG(
     text-anchor,
     text_anchor,
     TextAnchor,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kTextAnchorKTable,
@@ -3939,17 +3939,17 @@ CSS_PROP_TEXTRESET(
     TextDecorationStyle,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HK,
     kTextDecorationStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     text-emphasis,
     text_emphasis,
     TextEmphasis,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_TEXT(
     text-emphasis-color,
@@ -3967,28 +3967,28 @@ CSS_PROP_TEXT(
     text_emphasis_position,
     TextEmphasisPosition,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION,
     "",
     0,
     kTextEmphasisPositionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TEXT(
     text-emphasis-style,
     text_emphasis_style,
     TextEmphasisStyle,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TEXT(
     -webkit-text-fill-color,
     _webkit_text_fill_color,
     WebkitTextFillColor,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
         CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED,
@@ -4025,17 +4025,17 @@ CSS_PROP_TEXTRESET(
     TextOverflow,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     0,
     kTextOverflowKTable,
     offsetof(nsStyleTextReset, mTextOverflow),
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TEXT(
     text-rendering,
     text_rendering,
     TextRendering,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kTextRenderingKTable,
@@ -4060,17 +4060,17 @@ CSS_PROP_TEXT(
     -moz-text-size-adjust,
     text_size_adjust,
     CSS_PROP_DOMPROP_PREFIXED(TextSizeAdjust),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_AUTO | VARIANT_NONE | VARIANT_INHERIT,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     -webkit-text-stroke,
     _webkit_text_stroke,
     WebkitTextStroke,
     CSS_PROPERTY_PARSE_FUNCTION,
     "layout.css.prefixes.webkit")
 CSS_PROP_TEXT(
     -webkit-text-stroke-color,
@@ -4092,29 +4092,29 @@ CSS_PROP_TEXT(
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "layout.css.prefixes.webkit",
     VARIANT_HKL | VARIANT_CALC,
     kBorderWidthKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_TEXT(
     text-transform,
     text_transform,
     TextTransform,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HK,
     kTextTransformKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #ifndef CSS_PROP_LIST_ONLY_COMPONENTS_OF_ALL_SHORTHAND
 #ifndef CSS_PROP_LIST_EXCLUDE_INTERNAL
 CSS_PROP_FONT(
     -x-text-zoom,
     _x_text_zoom,
     TextZoom,
     CSS_PROPERTY_INTERNAL |
         CSS_PROPERTY_PARSE_INACCESSIBLE,
@@ -4157,17 +4157,17 @@ CSS_PROP_DISPLAY(
     touch_action,
     TouchAction,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_PARSER_FUNCTION,
     "layout.css.touch_action.enabled",
     VARIANT_HK,
     kTouchActionKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     transform,
     transform,
     Transform,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH |
         CSS_PROPERTY_CREATES_STACKING_CONTEXT |
         CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR |
@@ -4190,17 +4190,17 @@ CSS_PROP_DISPLAY(
     transform-box,
     transform_box,
     TransformBox,
     CSS_PROPERTY_PARSE_VALUE,
     "svg.transform-box.enabled",
     VARIANT_HK,
     kTransformBoxKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_DISPLAY(
     transform-origin,
     transform_origin,
     TransformOrigin,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH,
     "",
@@ -4214,17 +4214,17 @@ CSS_PROP_DISPLAY(
     TransformStyle,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_CREATES_STACKING_CONTEXT |
         CSS_PROPERTY_FIXPOS_CB,
     "",
     VARIANT_HK,
     kTransformStyleKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_SHORTHAND(
     transition,
     transition,
     Transition,
     CSS_PROPERTY_PARSE_FUNCTION,
     "")
 CSS_PROP_DISPLAY(
     transition-delay,
@@ -4275,58 +4275,58 @@ CSS_PROP_TEXTRESET(
     unicode-bidi,
     unicode_bidi,
     UnicodeBidi,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kUnicodeBidiKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #endif // CSS_PROP_LIST_ONLY_COMPONENTS_OF_ALL_SHORTHAND
 CSS_PROP_USERINTERFACE(
     -moz-user-focus,
     user_focus,
     CSS_PROP_DOMPROP_PREFIXED(UserFocus),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kUserFocusKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_USERINTERFACE(
     -moz-user-input,
     user_input,
     CSS_PROP_DOMPROP_PREFIXED(UserInput),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kUserInputKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX ??? // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX ??? // XXX bug 3935
 CSS_PROP_USERINTERFACE(
     -moz-user-modify,
     user_modify,
     CSS_PROP_DOMPROP_PREFIXED(UserModify),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kUserModifyKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_UIRESET(
     -moz-user-select,
     user_select,
     CSS_PROP_DOMPROP_PREFIXED(UserSelect),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kUserSelectKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None) // XXX bug 3935
+    eStyleAnimType_Discrete) // XXX bug 3935
 CSS_PROP_SVGRESET(
     vector-effect,
     vector_effect,
     VectorEffect,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kVectorEffectKTable,
@@ -4365,17 +4365,17 @@ CSS_PROP_TEXT(
     WhiteSpace,
     CSS_PROPERTY_PARSE_VALUE |
         // This is required by the UA stylesheet and can't be overridden.
         CSS_PROPERTY_APPLIES_TO_PLACEHOLDER,
     "",
     VARIANT_HK,
     kWhitespaceKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_POSITION(
     width,
     width,
     Width,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_VALUE_NONNEGATIVE |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
@@ -4390,27 +4390,27 @@ CSS_PROP_DISPLAY(
     will_change,
     WillChange,
     CSS_PROPERTY_PARSE_FUNCTION |
         CSS_PROPERTY_VALUE_LIST_USES_COMMAS,
     "",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_UIRESET(
     -moz-window-dragging,
     _moz_window_dragging,
     CSS_PROP_DOMPROP_PREFIXED(WindowDragging),
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kWindowDraggingKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 #ifndef CSS_PROP_LIST_EXCLUDE_INTERNAL
 CSS_PROP_UIRESET(
     -moz-window-shadow,
     _moz_window_shadow,
     CSS_PROP_DOMPROP_PREFIXED(WindowShadow),
     CSS_PROPERTY_INTERNAL |
         CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS_AND_CHROME,
@@ -4448,17 +4448,17 @@ CSS_PROP_TEXT(
     overflow-wrap,
     overflow_wrap,
     OverflowWrap,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kOverflowWrapKTable,
     CSS_PROP_NO_OFFSET,
-    eStyleAnimType_None)
+    eStyleAnimType_Discrete)
 CSS_PROP_VISIBILITY(
     writing-mode,
     writing_mode,
     WritingMode,
     CSS_PROPERTY_PARSE_VALUE,
     "",
     VARIANT_HK,
     kWritingModeKTable,
--- a/layout/style/test/test_animations.html
+++ b/layout/style/test/test_animations.html
@@ -45,17 +45,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   @keyframes kf2 {
     from { margin-top: 150px }
     50% { margin-top: 50px }
   }
   @keyframes kf3 {
     25% { margin-top: 100px }
   }
   @keyframes kf4 {
-    to, from { border-collapse: collapse; margin-top: 37px }
+    to, from { display: none; margin-top: 37px }
   }
   @keyframes kf_cascade1 {
     from { padding-top: 50px }
     50%, from { padding-top: 30px }      /* wins: 0% */
     75%, 85%, 50% { padding-top: 20px }  /* wins: 75%, 50% */
     100%, 85% { padding-top: 70px }      /* wins: 100% */
     85.1% { padding-top: 60px }          /* wins: 85.1% */
     85% { padding-top: 30px }            /* wins: 85% */
@@ -561,33 +561,33 @@ advance_clock(50);
 is(cs.marginTop, "50px", "no-0%-no-100% at 2.0s");
 done_div();
 
 // Test that non-animatable properties are ignored.
 // Simultaneously, test that the block is still honored, and that
 // we still override the value when two consecutive keyframes have
 // the same value.
 new_div("animation: kf4 ease 10s");
-is(cs.borderCollapse, "separate",
+is(cs.display, "block",
    "non-animatable properties should be ignored (linear, 0s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (linear, 0s)");
 advance_clock(1000);
-is(cs.borderCollapse, "separate",
+is(cs.display, "block",
    "non-animatable properties should be ignored (linear, 1s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (linear, 1s)");
 done_div();
 new_div("animation: kf4 step-start 10s");
-is(cs.borderCollapse, "separate",
+is(cs.display, "block",
    "non-animatable properties should be ignored (step-start, 0s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (step-start, 0s)");
 advance_clock(1000);
-is(cs.borderCollapse, "separate",
+is(cs.display, "block",
    "non-animatable properties should be ignored (step-start, 1s)");
 is(cs.marginTop, "37px",
    "animatable properties should still apply (step-start, 1s)");
 done_div();
 
 // Test cascading of the keyframes within an @keyframes rule.
 new_div("animation: kf_cascade1 linear 10s");
 //   0%: 30px
--- a/layout/style/test/test_animations_omta.html
+++ b/layout/style/test/test_animations_omta.html
@@ -56,17 +56,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     @keyframes kf2 {
       from { transform: translate(150px) }
       50% { transform: translate(50px) }
     }
     @keyframes kf3 {
       25% { transform: translate(100px) }
     }
     @keyframes kf4 {
-      to, from { border-collapse: collapse; transform: translate(37px) }
+      to, from { display: none; transform: translate(37px) }
     }
     @keyframes kf_cascade1 {
       from { transform: translate(50px) }
       50%, from { transform: translate(30px) }      /* wins: 0% */
       75%, 85%, 50% { transform: translate(20px) }  /* wins: 75%, 50% */
       100%, 85% { transform: translate(70px) }      /* wins: 100% */
       85.1% { transform: translate(60px) }          /* wins: 85.1% */
       85% { transform: translate(30px) }            /* wins: 85% */
@@ -629,35 +629,35 @@ addAsyncAnimTest(function *() {
 
   // Test that non-animatable properties are ignored.
   // Simultaneously, test that the block is still honored, and that
   // we still override the value when two consecutive keyframes have
   // the same value.
   new_div("animation: kf4 ease 10s");
   yield waitForPaintsFlushed();
   var cs = window.getComputedStyle(gDiv);
-  is(cs.borderCollapse, "separate",
+  is(cs.display, "block",
      "non-animatable properties should be ignored (linear, 0s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (linear, 0s)");
   advance_clock(1000);
-  is(cs.borderCollapse, "separate",
+  is(cs.display, "block",
      "non-animatable properties should be ignored (linear, 1s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (linear, 1s)");
   done_div();
   new_div("animation: kf4 step-start 10s");
   yield waitForPaintsFlushed();
   cs = window.getComputedStyle(gDiv);
-  is(cs.borderCollapse, "separate",
+  is(cs.display, "block",
      "non-animatable properties should be ignored (step-start, 0s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (step-start, 0s)");
   advance_clock(1000);
-  is(cs.borderCollapse, "separate",
+  is(cs.display, "block",
      "non-animatable properties should be ignored (step-start, 1s)");
   omta_is("transform", { tx: 37 }, RunningOn.Compositor,
           "animatable properties should still apply (step-start, 1s)");
   done_div();
 
   // Test cascading of the keyframes within an @keyframes rule.
   new_div("animation: kf_cascade1 linear 10s");
   yield waitForPaintsFlushed();
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -198,16 +198,17 @@ pref("dom.requestIdleCallback.enabled", 
 // Whether the Gamepad API is enabled
 pref("dom.gamepad.enabled", true);
 pref("dom.gamepad.test.enabled", false);
 #ifdef RELEASE_OR_BETA
 pref("dom.gamepad.non_standard_events.enabled", false);
 #else
 pref("dom.gamepad.non_standard_events.enabled", true);
 #endif
+pref("dom.gamepad.extensions.enabled", false);
 
 // Whether the KeyboardEvent.code is enabled
 pref("dom.keyboardevent.code.enabled", true);
 
 // If this is true, TextEventDispatcher dispatches keydown and keyup events
 // even during composition (keypress events are never fired during composition
 // even if this is true).
 pref("dom.keyboardevent.dispatch_during_composition", false);
--- a/testing/web-platform/meta/web-animations/animation-model/animation-types/type-per-property.html.ini
+++ b/testing/web-platform/meta/web-animations/animation-model/animation-types/type-per-property.html.ini
@@ -1,8 +1,12 @@
+prefs: [layout.css.contain.enabled:true,
+        layout.css.initial-letter.enabled:true,
+        layout.css.overflow-clip-box.enabled:true,
+        layout.css.shape-outside.enabled:true]
 [type-per-property.html]
   type: testharness
   [flex-basis supports animating as combination units 'px' and '%']
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1291187
 
   [flex-basis supports animating as combination units '%' and 'em']
     expected: FAIL
@@ -26,9 +30,8 @@
 
   [text-combine-upright uses discrete animation when animating between 'all' and 'digits' with effect easing]
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1258635
 
   [text-combine-upright uses discrete animation when animating between 'all' and 'digits' with keyframe easing]
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1258635
-
--- a/testing/web-platform/tests/web-animations/animation-model/animation-types/type-per-property.html
+++ b/testing/web-platform/tests/web-animations/animation-model/animation-types/type-per-property.html
@@ -29,40 +29,227 @@ var gCSSProperties = {
     ]
   },
   "align-self": {
     // https://drafts.csswg.org/css-align/#propdef-align-self
     tests: [
       discrete("flex-start", "flex-end")
     ]
   },
+  "backface-visibility": {
+    // https://drafts.csswg.org/css-transforms/#propdef-backface-visibility
+    "tests": [
+      discrete("visible", "hidden")
+    ]
+  },
+  "background-attachment": {
+    // https://drafts.csswg.org/css-backgrounds-3/#background-attachment
+    "tests": [
+      discrete("fixed", "local")
+    ]
+  },
+  "background-blend-mode": {
+    // https://drafts.fxtf.org/compositing-1/#propdef-background-blend-mode
+    "tests": [
+      discrete("multiply", "screen")
+    ]
+  },
+  "background-clip": {
+    // https://drafts.csswg.org/css-backgrounds-3/#background-clip
+    "tests": [
+      discrete("padding-box", "content-box")
+    ]
+  },
+  "background-image": {
+    // https://drafts.csswg.org/css-backgrounds-3/#background-image
+    "tests": [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "background-origin": {
+    // https://drafts.csswg.org/css-backgrounds-3/#background-origin
+    "tests": [
+      discrete("padding-box", "content-box")
+    ]
+  },
+  "background-repeat": {
+    // https://drafts.csswg.org/css-backgrounds-3/#background-repeat
+    "tests": [
+      discrete("space", "round")
+    ]
+  },
+  "border-bottom-style": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-bottom-style
+    "tests": [
+      discrete("dotted", "solid")
+    ]
+  },
+  "border-collapse": {
+    // https://drafts.csswg.org/css-tables/#propdef-border-collapse
+    "tests": [
+      discrete("collapse", "separate")
+    ]
+  },
+  "border-image-outset": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-image-outset
+    "tests": [
+      discrete("1 1 1 1", "5 5 5 5")
+    ]
+  },
+  "border-image-repeat": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-image-repeat
+    "tests": [
+      discrete("stretch stretch", "repeat repeat")
+    ]
+  },
+  "border-image-slice": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-image-slice
+    "tests": [
+      discrete("1 1 1 1", "5 5 5 5")
+    ]
+  },
+  "border-image-source": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-image-source
+    "tests": [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "border-image-width": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-image-width
+    "tests": [
+      discrete("1 1 1 1", "5 5 5 5")
+    ]
+  },
+  "border-left-style": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-left-style
+    "tests": [
+      discrete("dotted", "solid")
+    ]
+  },
+  "border-right-style": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-right-style
+    "tests": [
+      discrete("dotted", "solid")
+    ]
+  },
+  "border-top-style": {
+    // https://drafts.csswg.org/css-backgrounds-3/#border-top-style
+    "tests": [
+      discrete("dotted", "solid")
+    ]
+  },
+  "box-decoration-break": {
+    // https://drafts.csswg.org/css-break/#propdef-box-decoration-break
+    "tests": [
+      discrete("slice", "clone")
+    ]
+  },
+  "box-sizing": {
+    // https://drafts.csswg.org/css-ui-4/#box-sizing
+    "tests": [
+      discrete("content-box", "border-box")
+    ]
+  },
+  "caption-side": {
+    // https://drafts.csswg.org/css-tables/#propdef-caption-side
+    "tests": [
+      discrete("top", "bottom")
+    ]
+  },
+  "clear": {
+    // https://drafts.csswg.org/css-page-floats/#propdef-clear
+    "tests": [
+      discrete("inline-start", "inline-end")
+    ]
+  },
   "clip-rule": {
     // https://drafts.fxtf.org/css-masking-1/#propdef-clip-rule
     tests: [
       discrete("evenodd", "nonzero")
     ]
   },
+  "color-adjust": {
+    // https://drafts.csswg.org/css-color-4/#color-adjust
+    tests: [
+      discrete("economy", "exact")
+    ]
+  },
   "color-interpolation": {
     // https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty
     tests: [
       discrete("linearRGB", "auto")
     ]
   },
   "color-interpolation-filters": {
     // https://drafts.fxtf.org/filters-1/#propdef-color-interpolation-filters
     tests: [
       discrete("sRGB", "linearRGB")
     ]
   },
+  "column-fill": {
+    // https://drafts.csswg.org/css-multicol/#propdef-column-fill
+    tests: [
+      discrete("auto", "balance")
+    ]
+  },
+  "column-rule-style": {
+    // https://drafts.csswg.org/css-multicol/#propdef-column-rule-style
+    tests: [
+      discrete("none", "dotted")
+    ]
+  },
+  "contain": {
+    // https://drafts.csswg.org/css-containment/#propdef-contain
+    tests: [
+      discrete("strict", "none")
+    ]
+  },
+  "content": {
+    // https://drafts.csswg.org/css-content-3/#propdef-content
+    tests: [
+      discrete("\"a\"", "\"b\"")
+    ],
+    tagName: "::before"
+  },
+  "counter-increment": {
+    // https://drafts.csswg.org/css-lists-3/#propdef-counter-increment
+    tests: [
+      discrete("ident-1 1", "ident-2 2")
+    ]
+  },
+  "counter-reset": {
+    // https://drafts.csswg.org/css-lists-3/#propdef-counter-reset
+    tests: [
+      discrete("ident-1 1", "ident-2 2")
+    ]
+  },
+  "cursor": {
+    // https://drafts.csswg.org/css2/ui.html#propdef-cursor
+    tests: [
+      discrete("pointer", "wait")
+    ]
+  },
+  "direction": {
+    // https://drafts.csswg.org/css-writing-modes-3/#propdef-direction
+    tests: [
+      discrete("ltr", "rtl")
+    ]
+  },
   "dominant-baseline": {
     // https://drafts.csswg.org/css-inline/#propdef-dominant-baseline
     tests: [
       discrete("ideographic", "alphabetic")
     ]
   },
+  "empty-cells": {
+    // https://drafts.csswg.org/css-tables/#propdef-empty-cells
+    tests: [
+      discrete("show", "hide")
+    ]
+  },
   "fill-rule": {
     // https://svgwg.org/svg2-draft/painting.html#FillRuleProperty
     tests: [
       discrete("evenodd", "nonzero")
     ]
   },
   "flex-basis": {
     // https://drafts.csswg.org/css-flexbox/#propdef-flex-basis
@@ -96,20 +283,182 @@ var gCSSProperties = {
     ]
   },
   "font-style": {
     // https://drafts.csswg.org/css-fonts/#propdef-font-style
     tests: [
       discrete("italic", "oblique")
     ]
   },
-  "image-rendering": {
-    // https://drafts.csswg.org/css-images/#propdef-image-rendering
+  "float": {
+    // https://drafts.csswg.org/css-page-floats/#propdef-float
+    tests: [
+      discrete("left", "right")
+    ]
+  },
+  "font-family": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-family
+    tests: [
+      discrete("helvetica", "verdana")
+    ]
+  },
+  "font-feature-settings": {
+    // https://drafts.csswg.org/css-fonts/#descdef-font-feature-settings
+    tests: [
+      discrete("\"liga\" 5", "normal")
+    ]
+  },
+  "font-kerning": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-kerning
+    tests: [
+      discrete("auto", "normal")
+    ]
+  },
+  "font-language-override": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-language-override
+    tests: [
+      discrete("\"eng\"", "normal")
+    ]
+  },
+  "font-style": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-style
+    tests: [
+      discrete("italic", "oblique")
+    ]
+  },
+  "font-synthesis": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-synthesis
+    tests: [
+      discrete("none", "weight style")
+    ]
+  },
+  "font-variant-alternates": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-alternates
+    tests: [
+      discrete("swash(unknown)", "stylistic(unknown)")
+    ]
+  },
+  "font-variant-caps": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-caps
+    tests: [
+      discrete("small-caps", "unicase")
+    ]
+  },
+  "font-variant-east-asian": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-east-asian
+    tests: [
+      discrete("full-width", "proportional-width")
+    ]
+  },
+  "font-variant-ligatures": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-ligatures
+    tests: [
+      discrete("common-ligatures", "no-common-ligatures")
+    ]
+  },
+  "font-variant-numeric": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-numeric
+    tests: [
+      discrete("lining-nums", "oldstyle-nums")
+    ]
+  },
+  "font-variant-position": {
+    // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant-position
+    tests: [
+      discrete("sub", "super")
+    ]
+  },
+  "grid-auto-columns": {
+    // https://drafts.csswg.org/css-grid/#propdef-grid-auto-columns
     tests: [
-      discrete("optimizeQuality", "pixelated")
+      discrete("1px", "5px")
+    ]
+  },
+  "grid-auto-flow": {
+    // https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow
+    tests: [
+      discrete("row", "column")
+    ]
+  },
+  "grid-auto-rows": {
+    // https://drafts.csswg.org/css-grid/#propdef-grid-auto-rows
+    tests: [
+      discrete("1px", "5px")
+    ]
+  },
+  "grid-column-end": {
+    // https://drafts.csswg.org/css-grid/#propdef-grid-column-end
+    tests: [
+      discrete("1", "5")
+    ]
+  },
+  "grid-column-start": {
+    // https://drafts.csswg.org/css-grid/#propdef-grid-column-start
+    tests: [
+      discrete("1", "5")
+    ]
+  },
+  "grid-row-end": {
+    // https://drafts.csswg.org/css-grid/#propdef-grid-row-end
+    tests: [
+      discrete("1", "5")
+    ]
+  },
+  "grid-row-start": {
+    // https://drafts.csswg.org/css-grid/#propdef-grid-row-start
+    tests: [
+      discrete("1", "5")
+    ]
+  },
+  "grid-template-areas": {
+    // https://drafts.csswg.org/css-template/#grid-template-areas
+    tests: [
+      discrete("\". . a b\" \". .a b\"", "none")
+    ]
+  },
+  "grid-template-columns": {
+    // https://drafts.csswg.org/css-template/#grid-template-columns
+    tests: [
+      discrete("1px", "5px")
+    ]
+  },
+  "grid-template-rows": {
+    // https://drafts.csswg.org/css-template/#grid-template-rows
+    tests: [
+      discrete("1px", "5px")
+    ]
+  },
+  "hyphens": {
+    // https://drafts.csswg.org/css-text-3/#propdef-hyphens
+    tests: [
+      discrete("manual", "auto")
+    ]
+  },
+  "image-orientation": {
+    // https://drafts.csswg.org/css-images-3/#propdef-image-orientation
+    tests: [
+      discrete("0deg", "90deg")
+    ]
+  },
+  "ime-mode": {
+    // https://drafts.csswg.org/css-ui/#input-method-editor
+    tests: [
+      discrete("disabled", "auto")
+    ]
+  },
+  "initial-letter": {
+    // https://drafts.csswg.org/css-inline/#propdef-initial-letter
+    tests: [
+      discrete("1 2", "3 4")
+    ]
+  },
+  "isolation": {
+    // https://drafts.fxtf.org/compositing-1/#propdef-isolation
+    tests: [
+      discrete("auto", "isolate")
     ]
   },
   "justify-content": {
     // https://drafts.csswg.org/css-align/#propdef-justify-content
     tests: [
       discrete("baseline", "last baseline")
     ]
   },
@@ -120,47 +469,233 @@ var gCSSProperties = {
     ]
   },
   "justify-self": {
     // https://drafts.csswg.org/css-align/#propdef-justify-self
     tests: [
       discrete("baseline", "last baseline")
     ]
   },
+  "list-style-image": {
+    // https://drafts.csswg.org/css-lists-3/#propdef-list-style-image
+    tests: [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "list-style-position": {
+    // https://drafts.csswg.org/css-lists-3/#propdef-list-style-position
+    tests: [
+      discrete("inside", "outside")
+    ]
+  },
+  "list-style-type": {
+    // https://drafts.csswg.org/css-lists-3/#propdef-list-style-type
+    tests: [
+      discrete("circle", "square")
+    ]
+  },
+  "marker-end": {
+    // https://svgwg.org/specs/markers/#MarkerEndProperty
+    tests: [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "marker-mid": {
+    // https://svgwg.org/specs/markers/#MarkerMidProperty
+    tests: [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "marker-start": {
+    // https://svgwg.org/specs/markers/#MarkerStartProperty
+    tests: [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "mask": {
+    // https://drafts.fxtf.org/css-masking-1/#the-mask
+    tests: [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "mask-clip": {
+    // https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip
+    tests: [
+      discrete("content-box", "border-box")
+    ]
+  },
+  "mask-composite": {
+    // https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite
+    tests: [
+      discrete("add", "subtract")
+    ]
+  },
+  "mask-image": {
+    // https://drafts.fxtf.org/css-masking-1/#propdef-mask-image
+    tests: [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
+  "mask-mode": {
+    // https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode
+    tests: [
+      discrete("alpha", "luminance")
+    ]
+  },
+  "mask-origin": {
+    // https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin
+    tests: [
+      discrete("content-box", "border-box")
+    ]
+  },
+  "mask-repeat": {
+    // https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat
+    tests: [
+      discrete("space", "round")
+    ]
+  },
   "mask-type": {
     // https://drafts.fxtf.org/css-masking-1/#propdef-mask-type
     tests: [
       discrete("alpha", "luminance")
     ]
   },
+  "mix-blend-mode": {
+    // https://drafts.fxtf.org/compositing-1/#propdef-mix-blend-mode
+    tests: [
+      discrete("multiply", "screen")
+    ]
+  },
+  "object-fit": {
+    // https://drafts.csswg.org/css-images-3/#propdef-object-fit
+    tests: [
+      discrete("fill", "contain")
+    ]
+  },
   "order": {
     // https://drafts.csswg.org/css-flexbox/#propdef-order
     tests: [
       integer()
     ]
   },
+  "outline-style": {
+    // https://drafts.csswg.org/css-ui/#propdef-outline-style
+    tests: [
+      discrete("none", "dotted")
+    ]
+  },
+  "overflow-clip-box": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/overflow-clip-box
+    tests: [
+      discrete("padding-box", "content-box")
+    ]
+  },
+  "overflow-wrap": {
+    // https://drafts.csswg.org/css-text-3/#propdef-overflow-wrap
+    tests: [
+      discrete("normal", "break-word")
+    ]
+  },
+  "overflow-x": {
+    // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-x
+    tests: [
+      discrete("visible", "hidden")
+    ]
+  },
+  "overflow-y": {
+    // https://drafts.csswg.org/css-overflow-3/#propdef-overflow-y
+    tests: [
+      discrete("visible", "hidden")
+    ]
+  },
+  "page-break-after": {
+    // https://drafts.csswg.org/css-break-3/#propdef-break-after
+    tests: [
+      discrete("always", "auto")
+    ]
+  },
+  "page-break-before": {
+    // https://drafts.csswg.org/css-break-3/#propdef-break-before
+    tests: [
+      discrete("always", "auto")
+    ]
+  },
+  "page-break-inside": {
+    // https://drafts.csswg.org/css-break-3/#propdef-break-inside
+    tests: [
+      discrete("auto", "avoid")
+    ]
+  },
+  "paint-order": {
+    // https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty
+    tests: [
+      discrete("fill", "stroke")
+    ]
+  },
   "pointer-events": {
     // https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty
     tests: [
       discrete("fill", "none")
     ]
   },
+  "position": {
+    // https://drafts.csswg.org/css-position/#propdef-position
+    tests: [
+      discrete("absolute", "fixed")
+    ]
+  },
+  "quotes": {
+    // https://drafts.csswg.org/css-content-3/#propdef-quotes
+    tests: [
+      discrete("\"“\" \"”\" \"‘\" \"’\"", "\"‘\" \"’\" \"“\" \"”\"")
+    ]
+  },
+  "resize": {
+    // https://drafts.csswg.org/css-ui/#propdef-resize
+    tests: [
+      discrete("both", "horizontal")
+    ]
+  },
   "ruby-align": {
     // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-align
     tests: [
       discrete("start", "center")
     ]
   },
   "ruby-position": {
     // https://drafts.csswg.org/css-ruby-1/#propdef-ruby-position
     tests: [
       discrete("under", "over")
     ],
     tagName: "ruby"
   },
+  "scroll-behavior": {
+    // https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior
+    tests: [
+      discrete("auto", "smooth")
+    ]
+  },
+  "scroll-snap-type-x": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/scroll-snap-type-x
+    tests: [
+      discrete("mandatory", "proximity")
+    ]
+  },
+  "scroll-snap-type-y": {
+    // https://developer.mozilla.org/en/docs/Web/CSS/scroll-snap-type-y
+    tests: [
+      discrete("mandatory", "proximity")
+    ]
+  },
+  "shape-outside": {
+    // http://dev.w3.org/csswg/css-shapes/#propdef-shape-outside
+    tests: [
+      discrete("url(\"http://localhost/test-1\")", "url(\"http://localhost/test-2\")")
+    ]
+  },
   "shape-rendering": {
     // https://svgwg.org/svg2-draft/painting.html#ShapeRenderingProperty
     tests: [
       discrete("optimizeSpeed", "crispEdges")
     ]
   },
   "stroke-linecap": {
     // https://svgwg.org/svg2-draft/painting.html#StrokeLinecapProperty
@@ -170,64 +705,148 @@ var gCSSProperties = {
   },
   "stroke-linejoin": {
     // https://svgwg.org/svg2-draft/painting.html#StrokeLinejoinProperty
     tests: [
       discrete("round", "miter")
     ],
     tagName: "rect"
   },
+  "table-layout": {
+    // https://drafts.csswg.org/css-tables/#propdef-table-layout
+    tests: [
+      discrete("auto", "fixed")
+    ]
+  },
+  "text-align": {
+    // https://drafts.csswg.org/css-text-3/#propdef-text-align
+    tests: [
+      discrete("start", "end")
+    ]
+  },
+  "text-align-last": {
+    // https://drafts.csswg.org/css-text-3/#propdef-text-align-last
+    tests: [
+      discrete("start", "end")
+    ]
+  },
   "text-anchor": {
     // https://svgwg.org/svg2-draft/text.html#TextAnchorProperty
     tests: [
       discrete("middle", "end")
     ]
   },
   "text-combine-upright": {
     // https://drafts.csswg.org/css-writing-modes-3/#propdef-text-combine-upright
     tests: [
-      discrete("all", "digits")
+      discrete("all", "none")
     ]
   },
   "text-decoration-line": {
     // https://drafts.csswg.org/css-text-decor-3/#propdef-text-decoration-line
     tests: [
       discrete("underline", "overline")
     ]
   },
+  "text-decoration-style": {
+    // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-decoration-style
+    tests: [
+      discrete("solid", "dotted")
+    ]
+  },
+  "text-emphasis-position": {
+    // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-position
+    tests: [
+      discrete("over right", "under left")
+    ]
+  },
+  "text-emphasis-style": {
+    // http://dev.w3.org/csswg/css-text-decor-3/#propdef-text-emphasis-style
+    tests: [
+      discrete("filled circle", "open dot")
+    ]
+  },
   "text-orientation": {
     // https://drafts.csswg.org/css-writing-modes-3/#propdef-text-orientation
     tests: [
       discrete("upright", "sideways")
     ]
   },
+  "text-overflow": {
+    // https://drafts.csswg.org/css-ui/#propdef-text-overflow
+    tests: [
+      discrete("clip", "ellipsis")
+    ]
+  },
   "text-rendering": {
     // https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty
     tests: [
       discrete("optimizeSpeed", "optimizeLegibility")
     ]
   },
+  "text-transform": {
+    // https://drafts.csswg.org/css-text-3/#propdef-text-transform
+    tests: [
+      discrete("capitalize", "uppercase")
+    ]
+  },
+  "touch-action": {
+    // https://w3c.github.io/pointerevents/#the-touch-action-css-property
+    tests: [
+      discrete("auto", "none")
+    ]
+  },
+  "transform-box": {
+    // https://drafts.csswg.org/css-transforms/#propdef-transform-box
+    tests: [
+      discrete("fill-box", "border-box")
+    ]
+  },
+  "transform-style": {
+    // https://drafts.csswg.org/css-transforms/#propdef-transform-style
+    tests: [
+      discrete("flat", "preserve-3d")
+    ]
+  },
+  "unicode-bidi": {
+    // https://drafts.csswg.org/css-writing-modes-3/#propdef-unicode-bidi
+    tests: [
+      discrete("embed", "bidi-override")
+    ]
+  },
   "vector-effect": {
     // https://svgwg.org/svg2-draft/coords.html#VectorEffectProperty
     tests: [
       discrete("none", "non-scaling-stroke")
     ]
   },
   "visibility": {
     // https://drafts.csswg.org/css2/visufx.html#propdef-visibility
     tests: [
       visibility()
     ]
   },
+  "white-space": {
+    // https://drafts.csswg.org/css-text-4/#propdef-white-space
+    tests: [
+      discrete("pre", "nowrap")
+    ]
+  },
   "word-break": {
     // https://drafts.csswg.org/css-text-3/#propdef-word-break
     tests: [
       discrete("keep-all", "break-all")
     ]
   },
+  "will-change": {
+    // http://dev.w3.org/csswg/css-will-change/#propdef-will-change
+    tests: [
+      discrete("scroll-position", "contents")
+    ]
+  },
   "writing-mode": {
     // https://drafts.csswg.org/css-writing-modes-3/#propdef-writing-mode
     tests: [
       discrete("vertical-rl", "sideways-rl")
     ]
   },
 }
 
@@ -242,17 +861,17 @@ for (var property in gCSSProperties) {
 }
 
 function discrete(from, to) {
   return function(property, options) {
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = [from, to];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: from.toLowerCase() },
                             { time: 499,  expected: from.toLowerCase() },
                             { time: 500,  expected: to.toLowerCase() },
                             { time: 1000, expected: to.toLowerCase() }]);
     }, property + " uses discrete animation when animating between '"
@@ -260,17 +879,17 @@ function discrete(from, to) {
 
     test(function(t) {
       // Easing: http://cubic-bezier.com/#.68,0,1,.01
       // With this curve, we don't reach the 50% point until about 95% of
       // the time has expired.
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = [from, to];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both",
                                        easing: "cubic-bezier(0.68,0,1,0.01)" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: from.toLowerCase() },
                             { time: 940,  expected: from.toLowerCase() },
                             { time: 960,  expected: to.toLowerCase() }]);
     }, property + " uses discrete animation when animating between '"
@@ -279,17 +898,17 @@ function discrete(from, to) {
     test(function(t) {
       // Easing: http://cubic-bezier.com/#.68,0,1,.01
       // With this curve, we don't reach the 50% point until about 95% of
       // the time has expired.
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = [from, to];
       keyframes.easing = "cubic-bezier(0.68,0,1,0.01)";
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: from.toLowerCase() },
                             { time: 940,  expected: from.toLowerCase() },
                             { time: 960,  expected: to.toLowerCase() }]);
     }, property + " uses discrete animation when animating between '"
        + from + "' and '" + to + "' with keyframe easing");
@@ -297,81 +916,81 @@ function discrete(from, to) {
 }
 
 function length() {
   return function(property, options) {
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["10px", "50px"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "10px" },
                             { time: 500,  expected: "30px" },
                             { time: 1000, expected: "50px" }]);
     }, property + " supports animating as a length");
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["1rem", "5rem"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "10px" },
                             { time: 500,  expected: "30px" },
                             { time: 1000, expected: "50px" }]);
     }, property + " supports animating as a length of rem");
   }
 }
 
 function percentage() {
   return function(property, options) {
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["10%", "50%"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "10%" },
                             { time: 500,  expected: "30%" },
                             { time: 1000, expected: "50%" }]);
     }, property + " supports animating as a percentage");
   }
 }
 
 function integer() {
   return function(property, options) {
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = [-2, 2];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "-2" },
                             { time: 500,  expected: "0" },
                             { time: 1000, expected: "2" }]);
     }, property + " supports animating as an integer");
   }
 }
 
 function positiveNumber() {
   return function(property, options) {
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = [1.1, 1.5];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "1.1" },
                             { time: 500,  expected: "1.3" },
                             { time: 1000, expected: "1.5" }]);
     }, property + " supports animating as a positive number");
   }
@@ -381,69 +1000,69 @@ function lengthPercentageOrCalc() {
   return function(property, options) {
     length()(property, options);
     percentage()(property, options);
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["10px", "20%"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "10px" },
                             { time: 500,  expected: "calc(5px + 10%)" },
                             { time: 1000, expected: "20%" }]);
     }, property + " supports animating as combination units 'px' and '%'");
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["10%", "2em"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "10%" },
                             { time: 500,  expected: "calc(10px + 5%)" },
                             { time: 1000, expected: "20px" }]);
     }, property + " supports animating as combination units '%' and 'em'");
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["1em", "2rem"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "10px" },
                             { time: 500,  expected: "15px" },
                             { time: 1000, expected: "20px" }]);
     }, property + " supports animating as combination units 'em' and 'rem'");
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["10px", "calc(1em + 20%)"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "10px" },
                             { time: 500,  expected: "calc(10px + 10%)" },
                             { time: 1000, expected: "calc(10px + 20%)" }]);
     }, property + " supports animating as combination units 'px' and 'calc'");
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["calc(10px + 10%)", "calc(1em + 1rem + 20%)"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,
                               expected: "calc(10px + 10%)" },
                             { time: 500,
                               expected: "calc(15px + 15%)" },
                             { time: 1000,
@@ -453,45 +1072,45 @@ function lengthPercentageOrCalc() {
 }
 
 function visibility() {
   return function(property, options) {
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["visible", "hidden"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "visible" },
                             { time: 999,  expected: "visible" },
                             { time: 1000, expected: "hidden" }]);
     }, property + " uses visibility animation when animating "
        + "from 'visible' to 'hidden'");
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["hidden", "visible"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "hidden" },
                             { time: 1,    expected: "visible" },
                             { time: 1000, expected: "visible" }]);
     }, property + " uses visibility animation when animating "
      + "from 'hidden' to 'visible'");
 
     test(function(t) {
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["hidden", "collapse"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation = target.animate(keyframes,
                                      { duration: 1000, fill: "both" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "hidden" },
                             { time: 499,  expected: "hidden" },
                             { time: 500,  expected: "collapse" },
                             { time: 1000, expected: "collapse" }]);
     }, property + " uses visibility animation when animating "
@@ -499,17 +1118,17 @@ function visibility() {
 
     test(function(t) {
       // Easing: http://cubic-bezier.com/#.68,-.55,.26,1.55
       // With this curve, the value is less than 0 till about 34%
       // also more than 1 since about 63%
       var idlName = propertyToIDL(property);
       var keyframes = {};
       keyframes[idlName] = ["visible", "hidden"];
-      var target = createElement(t, options.tagName);
+      var target = createTestElement(t, options.tagName);
       var animation =
         target.animate(keyframes,
                        { duration: 1000, fill: "both",
                          easing: "cubic-bezier(0.68, -0.55, 0.26, 1.55)" });
       testAnimationSamples(animation, idlName,
                            [{ time: 0,    expected: "visible" },
                             { time: 1,    expected: "visible" },
                             { time: 330,  expected: "visible" },
@@ -518,25 +1137,35 @@ function visibility() {
                             { time: 630,  expected: "hidden" },
                             { time: 1000, expected: "hidden" }]);
     }, property + " uses visibility animation when animating "
      + "from 'visible' to 'hidden' with easeInOutBack easing");
   }
 }
 
 function testAnimationSamples(animation, idlName, testSamples) {
-  var target = animation.effect.target;
+  var type = animation.effect.target.type;
+  var target = type
+               ? animation.effect.target.parentElement
+               : animation.effect.target;
   testSamples.forEach(function(testSample) {
     animation.currentTime = testSample.time;
-    assert_equals(getComputedStyle(target)[idlName], testSample.expected,
+    assert_equals(getComputedStyle(target, type)[idlName],
+                  testSample.expected,
                   "The value should be " + testSample.expected +
                   " at " + testSample.time + "ms");
   });
 }
 
+function createTestElement(t, tagName) {
+  return tagName && tagName.startsWith("::")
+         ? createPseudo(t, tagName.substring(2))
+         : createElement(t, tagName);
+}
+
 function isSupported(property) {
   var testKeyframe = new TestKeyframe(propertyToIDL(property));
   try {
     // Since TestKeyframe returns 'undefined' for |property|,
     // the KeyframeEffect constructor will throw
     // if the string "undefined" is not a valid value for the property.
     new KeyframeEffect(null, testKeyframe);
   } catch(e) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/ExtensionSearchHandler.jsm
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "ExtensionSearchHandler" ];
+
+// Used to keep track of all of the registered keywords, where each keyword is
+// mapped to a KeywordInfo instance.
+let gKeywordMap = new Map();
+
+// Used to keep track of the active input session.
+let gActiveInputSession = null;
+
+// Used to keep track of who has control over the active suggestion callback
+// so older callbacks can be ignored. The callback ID should increment whenever
+// the input changes or the input session ends.
+let gCurrentCallbackID = 0;
+
+// Handles keeping track of information associated to the registered keyword.
+class KeywordInfo {
+  constructor(extension, description) {
+    this._extension = extension;
+    this._description = description;
+  }
+
+  get description() {
+    return this._description;
+  }
+
+  set description(desc) {
+    this._description = desc;
+  }
+
+  get extension() {
+    return this._extension;
+  }
+}
+
+// Responsible for handling communication between the extension and the urlbar.
+class InputSession {
+  constructor(keyword, extension) {
+    this._keyword = keyword;
+    this._extension = extension;
+    this._suggestionsCallback = null;
+    this._searchFinishedCallback = null;
+  }
+
+  get keyword() {
+    return this._keyword;
+  }
+
+  addSuggestions(suggestions) {
+    this._suggestionsCallback(suggestions);
+  }
+
+  start(eventName) {
+    this._extension.emit(eventName);
+  }
+
+  update(eventName, text, suggestionsCallback, searchFinishedCallback) {
+    if (this._searchFinishedCallback) {
+      this._searchFinishedCallback();
+    }
+    this._searchFinishedCallback = searchFinishedCallback;
+    this._suggestionsCallback = suggestionsCallback;
+    this._extension.emit(eventName, text, ++gCurrentCallbackID);
+  }
+
+  cancel(eventName) {
+    this._searchFinishedCallback();
+    this._extension.emit(eventName);
+  }
+
+  end(eventName, text, disposition) {
+    this._searchFinishedCallback();
+    this._extension.emit(eventName, text, disposition);
+  }
+}
+
+var ExtensionSearchHandler = Object.freeze({
+  MSG_INPUT_STARTED: "webext-omnibox-input-started",
+  MSG_INPUT_CHANGED: "webext-omnibox-input-changed",
+  MSG_INPUT_ENTERED: "webext-omnibox-input-entered",
+  MSG_INPUT_CANCELLED: "webext-omnibox-input-cancelled",
+
+  /**
+   * Registers a keyword.
+   *
+   * @param {string} keyword The keyword to register.
+   * @param {Extension} extension The extension registering the keyword.
+   */
+  registerKeyword(keyword, extension) {
+    if (gKeywordMap.has(keyword)) {
+      throw new Error(`The keyword provided is already registered: "${keyword}"`);
+    }
+    gKeywordMap.set(keyword, new KeywordInfo(extension, extension.name));
+  },
+
+  /**
+   * Unregisters a keyword.
+   *
+   * @param {string} keyword The keyword to unregister.
+   */
+  unregisterKeyword(keyword) {
+    if (!gKeywordMap.has(keyword)) {
+      throw new Error(`The keyword provided is not registered: "${keyword}"`);
+    }
+    gActiveInputSession = null;
+    gKeywordMap.delete(keyword);
+  },
+
+  /**
+   * Checks if a keyword is registered.
+   *
+   * @param {string} keyword The word to check.
+   * @return {boolean} true if the word is a registered keyword.
+   */
+  isKeywordRegistered(keyword) {
+    return gKeywordMap.has(keyword);
+  },
+
+  /**
+   * @return {boolean} true if there is an active input session.
+   */
+  hasActiveInputSession() {
+    return gActiveInputSession != null;
+  },
+
+  /**
+   * @param {string} keyword The keyword to look up.
+   * @return {string} the description to use for the heuristic result.
+   */
+  getDescription(keyword) {
+    if (!gKeywordMap.has(keyword)) {
+      throw new Error(`The keyword provided is not registered: "${keyword}"`);
+    }
+    return gKeywordMap.get(keyword).description;
+  },
+
+  /**
+   * Sets the default suggestion for the registered keyword. The suggestion's
+   * description will be used for the comment in the heuristic result.
+   *
+   * @param {string} keyword The keyword.
+   * @param {string} description The description to use for the heuristic result.
+   */
+  setDefaultSuggestion(keyword, {description}) {
+    if (!gKeywordMap.has(keyword)) {
+      throw new Error(`The keyword provided is not registered: "${keyword}"`);
+    }
+    gKeywordMap.get(keyword).description = description;
+  },
+
+  /**
+   * Adds suggestions for the registered keyword. This function will throw if
+   * the keyword provided is not registered or active, or if the callback ID
+   * provided is no longer equal to the active callback ID.
+   *
+   * @param {string} keyword The keyword.
+   * @param {integer} id The ID of the suggestion callback.
+   * @param {Array<Object>} suggestions An array of suggestions to provide to the urlbar.
+   */
+  addSuggestions(keyword, id, suggestions) {
+    if (!gKeywordMap.has(keyword)) {
+      throw new Error(`The keyword provided is not registered: "${keyword}"`);
+    }
+
+    if (!gActiveInputSession || gActiveInputSession.keyword != keyword) {
+      throw new Error(`The keyword provided is not apart of an active input session: "${keyword}"`);
+    }
+
+    if (id != gCurrentCallbackID) {
+      throw new Error(`The callback is no longer active for the keyword provided: "${keyword}"`);
+    }
+
+    gActiveInputSession.addSuggestions(suggestions);
+  },
+
+  /**
+   * Called when the input in the urlbar begins with `<keyword><space>`.
+   *
+   * If the keyword is inactive, MSG_INPUT_STARTED is emitted and the
+   * keyword is marked as active. If the keyword is followed by any text,
+   * MSG_INPUT_CHANGED is fired with the current callback ID that can be
+   * used to provide suggestions to the urlbar while the callback ID is active.
+   * The callback is invalidated when either the input changes or the urlbar blurs.
+   *
+   * @param {string} keyword The keyword to handle.
+   * @param {string} text The search text in the urlbar.
+   * @param {Function} callback The callback used to provide search suggestions.
+   * @return {Promise} promise that resolves when the current search is complete.
+   */
+  handleSearch(keyword, text, callback) {
+    if (!gKeywordMap.has(keyword)) {
+      throw new Error(`The keyword provided is not registered: "${keyword}"`);
+    }
+
+    if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
+      throw new Error("A different input session is already ongoing");
+    }
+
+    if (!text || !text.startsWith(`${keyword} `)) {
+      throw new Error(`The text provided must start with: "${keyword} "`);
+    }
+
+    if (!callback) {
+      throw new Error("A callback must be provided");
+    }
+
+    // The search text in the urlbar currently starts with <keyword><space>, and
+    // we only want the text that follows.
+    text = text.substring(keyword.length + 1);
+
+    // We fire MSG_INPUT_STARTED once we have <keyword><space>, and only fire
+    // MSG_INPUT_CHANGED when we have text to process. This is different from Chrome's
+    // behavior, which always fires MSG_INPUT_STARTED right before MSG_INPUT_CHANGED
+    // first fires, but this is a bug in Chrome according to https://crbug.com/258911.
+    if (!gActiveInputSession) {
+      gActiveInputSession = new InputSession(keyword, gKeywordMap.get(keyword).extension);
+      gActiveInputSession.start(this.MSG_INPUT_STARTED);
+
+      // Resolve early if there is no text to process. There can be text to process when
+      // the input starts if the user copy/pastes the text into the urlbar.
+      if (!text.length) {
+        return Promise.resolve();
+      }
+    }
+
+    return new Promise(resolve => {
+      gActiveInputSession.update(this.MSG_INPUT_CHANGED, text, callback, resolve);
+    });
+  },
+
+  /**
+   * Called when the user clicks on a suggestion that was added by
+   * an extension. MSG_INPUT_ENTERED is emitted to the extension with
+   * the keyword, the current search string, and info about how the
+   * the search should be handled. This ends the active input session.
+   *
+   * @param {string} keyword The keyword associated to the suggestion.
+   * @param {string} text The search text in the urlbar.
+   * @param {string} where How the page should be opened. Accepted values are:
+   *    "current": open the page in the same tab.
+   *    "tab": open the page in a new foreground tab.
+   *    "tabshifted": open the page in a new background tab.
+   */
+  handleInputEntered(keyword, text, where) {
+    if (!gKeywordMap.has(keyword)) {
+      throw new Error(`The keyword provided is not registered: "${keyword}"`);
+    }
+
+    if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
+      throw new Error("A different input session is already ongoing");
+    }
+
+    if (!text || !text.startsWith(`${keyword} `)) {
+      throw new Error(`The text provided must start with: "${keyword} "`);
+    }
+
+    let dispositionMap = {
+      current: "currentTab",
+      tab: "newForegroundTab",
+      tabshifted: "newBackgroundTab",
+    }
+    let disposition = dispositionMap[where];
+
+    if (!disposition) {
+      throw new Error(`Invalid "where" argument: ${where}`);
+    }
+
+    // The search text in the urlbar currently starts with <keyword><space>, and
+    // we only want to send the text that follows.
+    text = text.substring(keyword.length + 1);
+
+    gActiveInputSession.end(this.MSG_INPUT_ENTERED, text, disposition)
+    gActiveInputSession = null;
+  },
+
+  /**
+   * If the user has ended the keyword input session without accepting the input,
+   * MSG_INPUT_CANCELLED is emitted and the input session is ended.
+   */
+  handleInputCancelled() {
+    if (!gActiveInputSession) {
+      throw new Error("There is no active input session");
+    }
+    gActiveInputSession.cancel(this.MSG_INPUT_CANCELLED);
+    gActiveInputSession = null;
+  }
+});
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -64,16 +64,21 @@ const TELEMETRY_6_FIRST_RESULTS = "PLACE
 // The default frecency value used when inserting matches with unknown frecency.
 const FRECENCY_DEFAULT = 1000;
 
 // Remote matches are appended when local matches are below a given frecency
 // threshold (FRECENCY_DEFAULT) as soon as they arrive.  However we'll
 // always try to have at least MINIMUM_LOCAL_MATCHES local matches.
 const MINIMUM_LOCAL_MATCHES = 6;
 
+// Extensions are allowed to add suggestions if they have registered a keyword
+// with the omnibox API. This is the maximum number of suggestions an extension
+// is allowed to add for a given search string.
+const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
+
 // A regex that matches "single word" hostnames for whitelisting purposes.
 // The hostname will already have been checked for general validity, so we
 // don't need to be exhaustive here, so allow dashes anywhere.
 const REGEXP_SINGLEWORD_HOST = new RegExp("^[a-z0-9-]+$", "i");
 
 // Regex used to match userContextId.
 const REGEXP_USER_CONTEXT_ID = /(?:^| )user-context-id:(\d+)/;
 
@@ -264,16 +269,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
                                   "resource://gre/modules/Sqlite.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
+                                  "resource://gre/modules/ExtensionSearchHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
                                   "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesRemoteTabsAutocompleteProvider",
                                   "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
@@ -738,21 +745,26 @@ function Search(searchString, searchPara
   this._usedURLs = new Set();
   this._usedPlaceIds = new Set();
 
   // Resolved when all the remote matches have been fetched.
   this._remoteMatchesPromises = [];
 
   // The index to insert remote matches at.
   this._remoteMatchesStartIndex = 0;
+  // The index to insert local matches at.
+
+  this._localMatchesStartIndex = 0;
 
   // Counts the number of inserted local matches.
   this._localMatchesCount = 0;
   // Counts the number of inserted remote matches.
   this._remoteMatchesCount = 0;
+  // Counts the number of inserted extension matches.
+  this._extensionMatchesCount = 0;
 }
 
 Search.prototype = {
   /**
    * Enables the desired AutoComplete behavior.
    *
    * @param type
    *        The behavior type to set.
@@ -965,25 +977,45 @@ Search.prototype = {
       for (let [query, params] of [ this._adaptiveQuery,
                                     this._searchQuery ]) {
         yield conn.executeCached(query, params, this._onResultRow.bind(this));
         if (!this.pending)
           return;
       }
     }
 
-    // Ensure to fill any remaining space.
+    // Only add extension suggestions if the first token is a registered keyword
+    // and the search string has characters after the first token.
+    if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
+        this._originalSearchString.length > this._searchTokens[0].length) {
+      yield this._matchExtensionSuggestions();
+      if (!this.pending)
+        return;
+    } else if (ExtensionSearchHandler.hasActiveInputSession()) {
+      ExtensionSearchHandler.handleInputCancelled();
+    }
+
+    // Ensure to fill any remaining space. Suggestions which come from extensions are
+    // inserted at the beginning, so any suggestions
     yield Promise.all(this._remoteMatchesPromises);
   }),
 
   *_matchFirstHeuristicResult(conn) {
     // We always try to make the first result a special "heuristic" result.  The
     // heuristics below determine what type of result it will be, if any.
 
-    let hasSearchTerms = this._searchTokens.length > 0 ;
+    let hasSearchTerms = this._searchTokens.length > 0;
+
+    if (hasSearchTerms) {
+      // It may be a keyword registered by an extension.
+      let matched = yield this._matchExtensionHeuristicResult();
+      if (matched) {
+        return true;
+      }
+    }
 
     if (this._enableActions && hasSearchTerms) {
       // It may be a search engine with an alias - which works like a keyword.
       let matched = yield this._matchSearchEngineAlias();
       if (matched) {
         return true;
       }
     }
@@ -1147,16 +1179,26 @@ Search.prototype = {
     let [ query, params ] = this._hostQuery;
     yield conn.executeCached(query, params, row => {
       gotResult = true;
       this._onResultRow(row);
     });
     return gotResult;
   },
 
+  _matchExtensionHeuristicResult: function* () {
+    if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
+        this._originalSearchString.length > this._searchTokens[0].length) {
+      let description = ExtensionSearchHandler.getDescription(this._searchTokens[0]);
+      this._addExtensionMatch(this._originalSearchString, description);
+      return true;
+    }
+    return false;
+  },
+
   _matchPlacesKeyword: function* () {
     // The first word could be a keyword, so that's what we'll search.
     let keyword = this._searchTokens[0];
     let entry = yield PlacesUtils.keywords.fetch(this._searchTokens[0]);
     if (!entry)
       return false;
 
     let searchString = this._trimmedOriginalSearchString.substr(keyword.length + 1);
@@ -1259,16 +1301,34 @@ Search.prototype = {
     if (!match)
       return false;
 
     let query = this._originalSearchString;
     this._addSearchEngineMatch(match, query);
     return true;
   },
 
+  _addExtensionMatch(content, comment) {
+    if (this._extensionMatchesCount >= MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
+      return;
+    }
+
+    this._addMatch({
+      value: PlacesUtils.mozActionURI("extension", {
+        content,
+        keyword: this._searchTokens[0]
+      }),
+      comment,
+      icon: "chrome://browser/content/extension.svg",
+      style: "action extension",
+      frecency: FRECENCY_DEFAULT,
+      extension: true,
+    });
+  },
+
   _addSearchEngineMatch(match, query, suggestion) {
     let actionURLParams = {
       engineName: match.engineName,
       input: suggestion || this._originalSearchString,
       searchQuery: query,
     };
     if (suggestion)
       actionURLParams.searchSuggestion = suggestion;
@@ -1282,16 +1342,28 @@ Search.prototype = {
       comment: match.engineName,
       icon: match.iconUrl,
       style: "action searchengine",
       frecency: FRECENCY_DEFAULT,
       remote: !!suggestion
     });
   },
 
+  *_matchExtensionSuggestions() {
+    let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
+      suggestions => {
+        suggestions.forEach(suggestion => {
+          let content = `${this._searchTokens[0]} ${suggestion.content}`;
+          this._addExtensionMatch(content, suggestion.description);
+        });
+      }
+    );
+    this._remoteMatchesPromises.push(promise);
+  },
+
   *_matchRemoteTabs() {
     let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
     for (let {url, title, icon, deviceName} of matches) {
       // It's rare that Sync supplies the icon for the page (but if it does, it
       // is a string URL)
       if (!icon) {
         try {
           let favicon = yield PlacesUtils.promiseFaviconLinkUrl(url);
@@ -1487,16 +1559,21 @@ Search.prototype = {
   },
 
   _getInsertIndexForMatch(match) {
     let index = 0;
     if (match.remote) {
       // Append after local matches.
       index = this._remoteMatchesStartIndex + this._remoteMatchesCount;
       this._remoteMatchesCount++;
+    } else if (match.extension) {
+      index = this._localMatchesStartIndex;
+      this._localMatchesStartIndex++;
+      this._remoteMatchesStartIndex++;
+      this._extensionMatchesCount++;
     } else {
       // This is a local match.
       if (match.frecency > FRECENCY_DEFAULT ||
           this._localMatchesCount < MINIMUM_LOCAL_MATCHES) {
         // Append before remote matches.
         index = this._remoteMatchesStartIndex;
         this._remoteMatchesStartIndex++
       } else {
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -59,16 +59,17 @@ if CONFIG['MOZ_PLACES']:
 
     EXTRA_JS_MODULES += [
         'BookmarkHTMLUtils.jsm',
         'BookmarkJSONUtils.jsm',
         'Bookmarks.jsm',
         'ClusterLib.js',
         'ColorAnalyzer_worker.js',
         'ColorConversion.js',
+        'ExtensionSearchHandler.jsm',
         'History.jsm',
         'PlacesBackups.jsm',
         'PlacesDBUtils.jsm',
         'PlacesRemoteTabsAutocompleteProvider.jsm',
         'PlacesSearchAutocompleteProvider.jsm',
         'PlacesSyncUtils.jsm',
         'PlacesTransactions.jsm',
         'PlacesUtils.jsm',
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -162,30 +162,31 @@ function* check_autocomplete(test) {
                      .getService(Ci.nsIAutoCompleteController);
   controller.input = input;
 
   let numSearchesStarted = 0;
   input.onSearchBegin = () => {
     do_print("onSearchBegin received");
     numSearchesStarted++;
   };
-  let deferred = Promise.defer();
-  input.onSearchComplete = () => {
-    do_print("onSearchComplete received");
-    deferred.resolve();
-  }
-
+  let searchCompletePromise = new Promise(resolve => {
+    input.onSearchComplete = () => {
+      do_print("onSearchComplete received");
+      resolve();
+    }
+  });
   let expectedSearches = 1;
   if (test.incompleteSearch) {
     controller.startSearch(test.incompleteSearch);
     expectedSearches++;
   }
+
   do_print("Searching for: '" + test.search + "'");
   controller.startSearch(test.search);
-  yield deferred.promise;
+  yield searchCompletePromise;
 
   Assert.equal(numSearchesStarted, expectedSearches, "All searches started");
 
   // Check to see the expected uris and titles match up. If 'enable-actions'
   // is specified, we check that the first specified match is the first
   // controller value (as this is the "special" always selected item), but the
   // rest can match in any order.
   // If 'enable-actions' is not specified, they can match in any order.
@@ -410,16 +411,32 @@ function makeVisitMatch(input, url, extr
 function makeSwitchToTabMatch(url, extra = {}) {
   return {
     uri: makeActionURI("switchtab", {url}),
     title: extra.title || url,
     style: [ "action", "switchtab" ],
   }
 }
 
+function makeExtensionMatch(extra = {}) {
+  let style = [ "action", "extension" ];
+  if (extra.heuristic) {
+    style.push("heuristic");
+  }
+
+  return {
+    uri: makeActionURI("extension", {
+      content: extra.content,
+      keyword: extra.keyword,
+    }),
+    title: extra.description,
+    style,
+  };
+}
+
 function setFaviconForHref(href, iconHref) {
   return new Promise(resolve => {
     PlacesUtils.favicons.setAndFetchFaviconForPage(
       NetUtil.newURI(href),
       NetUtil.newURI(iconHref),
       true,
       PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
       resolve,
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
@@ -0,0 +1,384 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * 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/. */
+
+Cu.import("resource://gre/modules/ExtensionSearchHandler.jsm");
+
+let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(Ci.nsIAutoCompleteController);
+
+add_task(function* test_correct_errors_are_thrown() {
+  let keyword = "foo";
+  let anotherKeyword = "bar";
+  let unregisteredKeyword = "baz";
+
+  // Register a keyword.
+  ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
+
+  // Try registering the keyword again.
+  Assert.throws(() => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }));
+
+  // Register a different keyword.
+  ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
+
+  // Try calling handleSearch for an unregistered keyword.
+  Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `, () => {}));
+
+  // Try calling handleSearch without a callback.
+  Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `));
+
+  // Try getting the description for a keyword which isn't registered.
+  Assert.throws(() => ExtensionSearchHandler.getDescription(unregisteredKeyword));
+
+  // Try getting the extension name for a keyword which isn't registered.
+  Assert.throws(() => ExtensionSearchHandler.getExtensionName(unregisteredKeyword));
+
+  // Try setting the default suggestion for a keyword which isn't registered.
+  Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, "suggestion"));
+
+  // Try calling handleInputCancelled when there is no active input session.
+  Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
+
+  // Try calling handleInputEntered when there is no active input session.
+  Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
+
+  // Start a session by calling handleSearch with the registered keyword.
+  ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {});
+
+  // Try providing suggestions for an unregistered keyword.
+  Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []));
+
+  // Try providing suggestions for an inactive keyword.
+  Assert.throws(() => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []));
+
+  // Try calling handleSearch for an inactive keyword.
+  Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} `, () => {}));
+
+  // Try calling addSuggestions with an old callback ID.
+  Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 0, []));
+
+  // Add suggestions with a valid callback ID.
+  ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+  // Add suggestions again with a valid callback ID.
+  ExtensionSearchHandler.addSuggestions(keyword, 1, []);
+
+  // Try calling addSuggestions with a future callback ID.
+  Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
+
+  // End the input session by calling handleInputCancelled.
+  ExtensionSearchHandler.handleInputCancelled();
+
+  // Try calling handleInputCancelled after the session has ended.
+  Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
+
+  // Try calling handleSearch that doesn't have a space after the keyword.
+  Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword}`, () => {}));
+
+  // Try calling handleSearch with text starting with the wrong keyword.
+  Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${keyword} test`, () => {}));
+
+  // Start a new session by calling handleSearch with a different keyword
+  ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} test`, () => {});
+
+  // Try adding suggestions again with the same callback ID now that the input session has ended.
+  Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 1, []));
+
+  // Add suggestions with a valid callback ID.
+  ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
+
+  // Try adding suggestions with a valid callback ID but a different keyword.
+  Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
+
+  // Try adding suggestions with a valid callback ID but an unregistered keyword.
+  Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []));
+
+  // Set the default suggestion.
+  ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {description: "test result"});
+
+  // Try ending the session using handleInputEntered with a different keyword.
+  Assert.throws(() => ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} test`, "tab"));
+
+  // Try calling handleInputEntered with invalid text.
+  Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"));
+
+  // Try calling handleInputEntered with an invalid disposition.
+  Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "invalid"));
+
+  // End the session by calling handleInputEntered.
+  ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab");
+
+  // Try calling handleInputEntered after the session has ended.
+  Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
+
+  // Unregister the keyword.
+  ExtensionSearchHandler.unregisterKeyword(keyword);
+
+  // Try setting the default suggestion for the unregistered keyword.
+  Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(keyword, {description: "test"}));
+
+  // Try handling a search with the unregistered keyword.
+  Assert.throws(() => ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {}));
+
+  // Try unregistering the keyword again.
+  Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(keyword));
+
+  // Unregister the other keyword.
+  ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
+
+  // Try unregistering the word which was never registered.
+  Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword));
+
+  // Try setting the default suggestion for a word that was never registered.
+  Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {description: "test"}));
+
+  yield cleanup();
+});
+
+add_task(function* test_correct_events_are_emitted() {
+  let events = [];
+  function checkEvents(expectedEvents) {
+    Assert.equal(events.length, expectedEvents.length, "The correct number of events fired");
+    expectedEvents.forEach((e, i) => Assert.equal(e, events[i], `Expected "${e}" event to fire`));
+    events = [];
+  }
+
+  let mockExtension = { emit: message => events.push(message) };
+
+  let keyword = "foo";
+  let anotherKeyword = "bar";
+
+  ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+  ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
+
+  ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
+  checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
+
+  ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
+  checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
+
+  ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} f`, "tab");
+  checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+  ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
+  checkEvents([
+    ExtensionSearchHandler.MSG_INPUT_STARTED,
+    ExtensionSearchHandler.MSG_INPUT_CHANGED
+  ]);
+
+  ExtensionSearchHandler.handleInputCancelled();
+  checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
+
+  ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} baz`, () => {});
+  checkEvents([
+    ExtensionSearchHandler.MSG_INPUT_STARTED,
+    ExtensionSearchHandler.MSG_INPUT_CHANGED
+  ]);
+
+  ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} baz`, "tab");
+  checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
+
+  ExtensionSearchHandler.unregisterKeyword(keyword);
+});
+
+add_task(function* test_removes_suggestion_if_its_content_is_typed_in() {
+  let keyword = "test";
+  let extensionName = "Foo Bar";
+
+  let mockExtension = {
+    name: extensionName,
+    emit(message, text, id) {
+      if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+        ExtensionSearchHandler.addSuggestions(keyword, id, [
+          {content: "foo", description: "first suggestion"},
+          {content: "bar", description: "second suggestion"},
+          {content: "baz", description: "third suggestion"},
+        ]);
+        controller.stopSearch();
+      }
+    }
+  };
+
+  ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+  yield check_autocomplete({
+    search: `${keyword} unmatched`,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} unmatched`}),
+      makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
+    ]
+  });
+
+  yield check_autocomplete({
+    search: `${keyword} foo`,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} foo`}),
+      makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
+    ]
+  });
+
+  yield check_autocomplete({
+    search: `${keyword} bar`,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} bar`}),
+      makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
+    ]
+  });
+
+  yield check_autocomplete({
+    search: `${keyword} baz`,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} baz`}),
+      makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"})
+    ]
+  });
+
+  ExtensionSearchHandler.unregisterKeyword(keyword);
+  yield cleanup();
+});
+
+add_task(function* test_extension_results_should_come_first() {
+  let keyword = "test";
+  let extensionName = "Omnibox Example";
+
+  let uri = NetUtil.newURI(`http://a.com/b`);
+  yield PlacesTestUtils.addVisits([
+    { uri, title: `${keyword} -` },
+  ]);
+
+  let mockExtension = {
+    name: extensionName,
+    emit(message, text, id) {
+      if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+        ExtensionSearchHandler.addSuggestions(keyword, id, [
+          {content: "foo", description: "first suggestion"},
+          {content: "bar", description: "second suggestion"},
+          {content: "baz", description: "third suggestion"},
+        ]);
+      }
+      controller.stopSearch();
+    }
+  };
+
+  ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+  // Start an input session before testing MSG_INPUT_CHANGED.
+  ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
+
+  yield check_autocomplete({
+    search: `${keyword} -`,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} -`}),
+      makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}),
+      { uri, title: `${keyword} -` }
+    ]
+  });
+
+  ExtensionSearchHandler.unregisterKeyword(keyword);
+  yield cleanup();
+});
+
+add_task(function* test_setting_the_default_suggestion() {
+  let keyword = "test";
+  let extensionName = "Omnibox Example";
+
+  let mockExtension = {
+    name: extensionName,
+    emit(message, text, id) {
+      if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+        ExtensionSearchHandler.addSuggestions(keyword, id, []);
+      }
+      controller.stopSearch();
+    }
+  };
+
+  ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+  ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+    description: "hello world"
+  });
+
+  let searchString = `${keyword} search query`;
+  yield check_autocomplete({
+    search: searchString,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: "hello world", content: searchString}),
+    ]
+  });
+
+  ExtensionSearchHandler.setDefaultSuggestion(keyword, {
+    description: "foo bar"
+  });
+
+  yield check_autocomplete({
+    search: searchString,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: "foo bar", content: searchString}),
+    ]
+  });
+
+  ExtensionSearchHandler.unregisterKeyword(keyword);
+  yield cleanup();
+});
+
+add_task(function* test_maximum_number_of_suggestions_is_enforced() {
+  let keyword = "test";
+  let extensionName = "Omnibox Example";
+
+  let mockExtension = {
+    name: extensionName,
+    emit(message, text, id) {
+      if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
+        ExtensionSearchHandler.addSuggestions(keyword, id, [
+          {content: "a", description: "first suggestion"},
+          {content: "b", description: "second suggestion"},
+          {content: "c", description: "third suggestion"},
+          {content: "d", description: "fourth suggestion"},
+          {content: "e", description: "fifth suggestion"},
+          {content: "f", description: "sixth suggestion"},
+          {content: "g", description: "seventh suggestion"},
+          {content: "h", description: "eigth suggestion"},
+          {content: "i", description: "ninth suggestion"},
+          {content: "j", description: "tenth suggestion"},
+        ]);
+        controller.stopSearch();
+      }
+    }
+  };
+
+  ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
+
+  // Start an input session before testing MSG_INPUT_CHANGED.
+  ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
+
+  yield check_autocomplete({
+    search: `${keyword} #`,
+    searchParam: "enable-actions",
+    matches: [
+      makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} #`}),
+      makeExtensionMatch({keyword, content: `${keyword} a`, description: "first suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} b`, description: "second suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} c`, description: "third suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} d`, description: "fourth suggestion"}),
+      makeExtensionMatch({keyword, content: `${keyword} e`, description: "fifth suggestion"}),
+    ]
+  });
+
+  ExtensionSearchHandler.unregisterKeyword(keyword);
+  yield cleanup();
+});
--- a/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
+++ b/toolkit/components/places/tests/unifiedcomplete/xpcshell.ini
@@ -19,16 +19,17 @@ support-files =
 [test_avoid_stripping_to_empty_tokens.js]
 [test_casing.js]
 [test_do_not_trim.js]
 [test_download_embed_bookmarks.js]
 [test_dupe_urls.js]
 [test_empty_search.js]
 [test_enabled.js]
 [test_escape_self.js]
+[test_extension_matches.js]
 [test_ignore_protocol.js]
 [test_keyword_search.js]
 [test_keyword_search_actions.js]
 [test_keywords.js]
 [test_match_beginning.js]
 [test_multi_word_search.js]
 [test_query_url.js]
 [test_remote_tab_matches.js]
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -2122,16 +2122,20 @@ extends="chrome://global/content/binding
               }
             } else if (action.type == "visiturl") {
               emphasiseUrl = false;
               displayUrl = this._unescapeUrl(action.params.url);
               title = displayUrl;
               titleLooksLikeUrl = true;
               let visitStr = this._stringBundle.GetStringFromName("visit");
               this._setUpDescription(this._actionText, visitStr, true);
+            } else if (action.type == "extension") {
+              let content = action.params.content;
+              displayUrl = content;
+              this._setUpDescription(this._actionText, content, true);
             }
           }
 
           if (!displayUrl) {
             let input = popup.input;
             let url = typeof(input.trimValue) == "function" ?
                       input.trimValue(originalUrl) :
                       originalUrl;
--- a/widget/windows/KeyboardLayout.cpp
+++ b/widget/windows/KeyboardLayout.cpp
@@ -3492,21 +3492,28 @@ KeyboardLayout::Shutdown()
 
 // static
 void
 KeyboardLayout::NotifyIdleServiceOfUserActivity()
 {
   sIdleService->ResetIdleTimeOut(0);
 }
 
-KeyboardLayout::KeyboardLayout() :
-  mKeyboardLayout(0), mIsOverridden(false),
-  mIsPendingToRestoreKeyboardLayout(false)
+KeyboardLayout::KeyboardLayout()
+  : mKeyboardLayout(0)
+  , mIsOverridden(false)
+  , mIsPendingToRestoreKeyboardLayout(false)
 {
   mDeadKeyTableListHead = nullptr;
+  // A dead key sequence should be made from up to 5 keys.  Therefore, 4 is
+  // enough and makes sense because the item is uint8_t.
+  // (Although, even if it's possible to be 6 keys or more in a sequence,
+  // this array will be re-allocated).
+  mActiveDeadKeys.SetCapacity(4);
+  mDeadKeyShiftStates.SetCapacity(4);
 
   // NOTE: LoadLayout() should be called via OnLayoutChange().
 }
 
 KeyboardLayout::~KeyboardLayout()
 {
   ReleaseDeadKeyTables();
 }
@@ -3608,45 +3615,44 @@ KeyboardLayout::InitNativeKey(NativeKey&
     // If it's not in dead key sequence, we don't need to do anymore here.
     if (!IsInDeadKeySequence()) {
       return;
     }
 
     // If it's in dead key sequence and dead char is inputted as is, we need to
     // set the previous modifier state which is stored when preceding dead key
     // is pressed.
-    UniCharsAndModifiers deadChars =
-      GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
+    UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
     aNativeKey.mCommittedCharsAndModifiers.
                  OverwriteModifiersIfBeginsWith(deadChars);
     // Finish the dead key sequence.
     DeactivateDeadKeyState();
     return;
   }
 
+  // If it's a dead key, aNativeKey will be initialized by
+  // MaybeInitNativeKeyAsDeadKey().
+  if (MaybeInitNativeKeyAsDeadKey(aNativeKey, aModKeyState)) {
+    return;
+  }
+
   // If the key is not a usual printable key, KeyboardLayout class assume that
   // it's not cause dead char nor printable char.  Therefore, there are nothing
   // to do here fore such keys (e.g., function keys).
   // However, this should keep dead key state even if non-printable key is
   // pressed during a dead key sequence.
   if (!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode)) {
     return;
   }
 
   MOZ_ASSERT(aNativeKey.mOriginalVirtualKeyCode != VK_PACKET,
     "At handling VK_PACKET, we shouldn't refer keyboard layout");
   MOZ_ASSERT(aNativeKey.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING,
     "Printable key's key name index must be KEY_NAME_INDEX_USE_STRING");
 
-  // If it's a dead key, aNativeKey will be initialized by
-  // MaybeInitNativeKeyAsDeadKey().
-  if (MaybeInitNativeKeyAsDeadKey(aNativeKey, aModKeyState)) {
-    return;
-  }
-
   // If it's in dead key handling and the pressed key causes a composite
   // character, aNativeKey will be initialized by
   // MaybeInitNativeKeyWithCompositeChar().
   if (MaybeInitNativeKeyWithCompositeChar(aNativeKey, aModKeyState)) {
     return;
   }
 
   UniCharsAndModifiers baseChars =
@@ -3654,59 +3660,53 @@ KeyboardLayout::InitNativeKey(NativeKey&
 
   // If the key press isn't related to any dead keys, initialize aNativeKey
   // with the characters which should be caused by the key.
   if (!IsInDeadKeySequence()) {
     aNativeKey.mCommittedCharsAndModifiers = baseChars;
     return;
   }
 
-  // Although, this shouldn't occur, if active dead key isn't a printable
-  // key, we cannot handle it because KeyboardLayout assumes that dead key
-  // is never mapped to non-printable keys (e.g., F4, etc).  Please be aware,
-  // it's possible, but we've not known such special keyboard layout yet.
-  if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey))) {
-    return;
-  }
-
   // If the key doesn't cause a composite character with preceding dead key,
   // initialize aNativeKey with the dead-key character followed by current
   // key's character.
-  UniCharsAndModifiers deadChars =
-    GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
+  UniCharsAndModifiers deadChars = GetDeadUniCharsAndModifiers();
   aNativeKey.mCommittedCharsAndModifiers = deadChars + baseChars;
   if (aNativeKey.IsKeyDownMessage()) {
     DeactivateDeadKeyState();
   }
 }
 
 bool
 KeyboardLayout::MaybeInitNativeKeyAsDeadKey(
                   NativeKey& aNativeKey,
                   const ModifierKeyState& aModKeyState)
 {
-  if (!IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
+  // Only when it's not in dead key sequence, we can trust IsDeadKey() result.
+  if (!IsInDeadKeySequence() &&
+      !IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
     return false;
   }
 
-  // If it's a keydown event but not in dead key sequence or it's a keyup
-  // event of a dead key which activated current dead key sequence,
-  // initialize aNativeKey as a dead key event.
-  if ((aNativeKey.IsKeyDownMessage() && !IsInDeadKeySequence()) ||
-      (!aNativeKey.IsKeyDownMessage() &&
-       mActiveDeadKey == aNativeKey.mOriginalVirtualKeyCode)) {
+  // When keydown message is followed by a dead char message, it should be
+  // initialized as dead key.
+  bool isDeadKeyDownEvent =
+    aNativeKey.IsKeyDownMessage() &&
+    aNativeKey.IsFollowedByDeadCharMessage();
+
+  // When keyup message is received, let's check if it's one of preceding
+  // dead keys because keydown message order and keyup message order may be
+  // different.
+  bool isDeadKeyUpEvent =
+    !aNativeKey.IsKeyDownMessage() &&
+    mActiveDeadKeys.Contains(aNativeKey.mOriginalVirtualKeyCode);
+
+  if (isDeadKeyDownEvent || isDeadKeyUpEvent) {
     ActivateDeadKeyState(aNativeKey, aModKeyState);
-#ifdef DEBUG
-    UniCharsAndModifiers deadChars =
-      GetNativeUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode,
-                                    aModKeyState);
-    MOZ_ASSERT(deadChars.Length() == 1,
-               "dead key must generate only one character");
-#endif
-    // First dead key event doesn't generate characters.  Dead key should
+    // Any dead key events don't generate characters.  So, a dead key should
     // cause only keydown event and keyup event whose KeyboardEvent.key
     // values are "Dead".
     aNativeKey.mCommittedCharsAndModifiers.Clear();
     aNativeKey.mKeyNameIndex = KEY_NAME_INDEX_Dead;
     return true;
   }
 
   // At keydown message handling, we need to forget the first dead key
@@ -3715,47 +3715,37 @@ KeyboardLayout::MaybeInitNativeKeyAsDead
   // another dead key before releasing current key.  Therefore, we can
   // set only a character for current key for keyup event.
   if (!IsInDeadKeySequence()) {
     aNativeKey.mCommittedCharsAndModifiers =
       GetUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode, aModKeyState);
     return true;
   }
 
+  // When non-printable key event comes during a dead key sequence, that must
+  // be a modifier key event.  So, such events shouldn't be handled as a part
+  // of the dead key sequence.
+  if (!IsDeadKey(aNativeKey.mOriginalVirtualKeyCode, aModKeyState)) {
+    return false;
+  }
+
   // FYI: Following code may run when the user doesn't input text actually
   //      but the key sequence is a dead key sequence.  For example,
   //      ` -> Ctrl+` with Spanish keyboard layout.  Let's keep using this
   //      complicated code for now because this runs really rarely.
 
-  if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey))) {
-#if defined(DEBUG) || defined(MOZ_CRASHREPORTER)
-    nsPrintfCString warning("The virtual key index (%d) of mActiveDeadKey "
-                            "(0x%02X) is not a printable key "
-                            "(aNativeKey.mOriginalVirtualKeyCode=0x%02X)",
-                            GetKeyIndex(mActiveDeadKey), mActiveDeadKey,
-                            aNativeKey.mOriginalVirtualKeyCode);
-    NS_WARNING(warning.get());
-#ifdef MOZ_CRASHREPORTER
-    CrashReporter::AppendAppNotesToCrashReport(
-                     NS_LITERAL_CSTRING("\n") + warning);
-#endif // #ifdef MOZ_CRASHREPORTER
-#endif // #if defined(DEBUG) || defined(MOZ_CRASHREPORTER)
-    MOZ_CRASH("Trying to reference out of range of mVirtualKeys");
-  }
-
   // Dead key followed by another dead key may cause a composed character
   // (e.g., "Russian - Mnemonic" keyboard layout's 's' -> 'c').
   if (MaybeInitNativeKeyWithCompositeChar(aNativeKey, aModKeyState)) {
     return true;
   }
 
   // Otherwise, dead key followed by another dead key causes inputting both
   // character.
-  UniCharsAndModifiers prevDeadChars =
-    GetUniCharsAndModifiers(mActiveDeadKey, mDeadKeyShiftState);
+  UniCharsAndModifiers prevDeadChars = GetDeadUniCharsAndModifiers();
   UniCharsAndModifiers newChars =
     GetUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode, aModKeyState);
   // But keypress events should be fired for each committed character.
   aNativeKey.mCommittedCharsAndModifiers = prevDeadChars + newChars;
   if (aNativeKey.IsKeyDownMessage()) {
     DeactivateDeadKeyState();
   }
   return true;
@@ -3765,29 +3755,27 @@ bool
 KeyboardLayout::MaybeInitNativeKeyWithCompositeChar(
                   NativeKey& aNativeKey,
                   const ModifierKeyState& aModKeyState)
 {
   if (!IsInDeadKeySequence()) {
     return false;
   }
 
-  if (NS_WARN_IF(!IsPrintableCharKey(mActiveDeadKey)) ||
-      NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) {
+  if (NS_WARN_IF(!IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode))) {
     return false;
   }
 
   UniCharsAndModifiers baseChars =
     GetUniCharsAndModifiers(aNativeKey.mOriginalVirtualKeyCode, aModKeyState);
   if (baseChars.IsEmpty() || !baseChars.CharAt(0)) {
     return false;
   }
 
-  char16_t compositeChar =
-    GetCompositeChar(mActiveDeadKey, mDeadKeyShiftState, baseChars.CharAt(0));
+  char16_t compositeChar = GetCompositeChar(baseChars.CharAt(0));
   if (!compositeChar) {
     return false;
   }
 
   // Active dead-key and base character does produce exactly one composite
   // character.
   aNativeKey.mCommittedCharsAndModifiers.Append(compositeChar,
                                                 baseChars.ModifiersAt(0));
@@ -3819,26 +3807,53 @@ KeyboardLayout::GetNativeUniCharsAndModi
   if (key < 0) {
     return UniCharsAndModifiers();
   }
   VirtualKey::ShiftState shiftState =
     VirtualKey::ModifierKeyStateToShiftState(aModKeyState);
   return mVirtualKeys[key].GetNativeUniChars(shiftState);
 }
 
+UniCharsAndModifiers
+KeyboardLayout::GetDeadUniCharsAndModifiers() const
+{
+  MOZ_RELEASE_ASSERT(mActiveDeadKeys.Length() == mDeadKeyShiftStates.Length());
+
+  if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
+    return UniCharsAndModifiers();
+  }
+
+  UniCharsAndModifiers result;
+  for (size_t i = 0; i < mActiveDeadKeys.Length(); ++i) {
+    result +=
+      GetUniCharsAndModifiers(mActiveDeadKeys[i], mDeadKeyShiftStates[i]);
+  }
+  return result;
+}
+
 char16_t
-KeyboardLayout::GetCompositeChar(uint8_t aVirtualKeyOfDeadKey,
-                                 VirtualKey::ShiftState aShiftStateOfDeadKey,
-                                 char16_t aBaseChar) const
+KeyboardLayout::GetCompositeChar(char16_t aBaseChar) const
 {
-  int32_t key = GetKeyIndex(aVirtualKeyOfDeadKey);
+  if (NS_WARN_IF(mActiveDeadKeys.IsEmpty())) {
+    return 0;
+  }
+  // XXX Currently, we don't support computing a composite character with
+  //     two or more dead keys since it needs big table for supporting
+  //     long chained dead keys.  However, this should be a minor bug
+  //     because this runs only when the latest keydown event does not cause
+  //     WM_(SYS)CHAR messages.  So, when user wants to input a character,
+  //     this path never runs.
+  if (mActiveDeadKeys.Length() > 1) {
+    return 0;
+  }
+  int32_t key = GetKeyIndex(mActiveDeadKeys[0]);
   if (key < 0) {
     return 0;
   }
-  return mVirtualKeys[key].GetCompositeChar(aShiftStateOfDeadKey, aBaseChar);
+  return mVirtualKeys[key].GetCompositeChar(mDeadKeyShiftStates[0], aBaseChar);
 }
 
 void
 KeyboardLayout::LoadLayout(HKL aLayout)
 {
   mIsPendingToRestoreKeyboardLayout = false;
 
   if (mKeyboardLayout == aLayout) {
@@ -3855,17 +3870,18 @@ KeyboardLayout::LoadLayout(HKL aLayout)
 
   BYTE originalKbdState[256];
   // Bitfield with all shift states that have at least one dead-key.
   uint16_t shiftStatesWithDeadKeys = 0;
   // Bitfield with all shift states that produce any possible dead-key base
   // characters.
   uint16_t shiftStatesWithBaseChars = 0;
 
-  mActiveDeadKey = -1;
+  mActiveDeadKeys.Clear();
+  mDeadKeyShiftStates.Clear();
 
   ReleaseDeadKeyTables();
 
   ::GetKeyboardState(originalKbdState);
 
   // For each shift state gather all printable characters that are produced
   // for normal case when no any dead-key is active.
 
@@ -4094,36 +4110,36 @@ void
 KeyboardLayout::ActivateDeadKeyState(const NativeKey& aNativeKey,
                                      const ModifierKeyState& aModKeyState)
 {
   // Dead-key state should be activated at keydown.
   if (!aNativeKey.IsKeyDownMessage()) {
     return;
   }
 
-  MOZ_RELEASE_ASSERT(IsPrintableCharKey(aNativeKey.mOriginalVirtualKeyCode));
-
-  mActiveDeadKey = aNativeKey.mOriginalVirtualKeyCode;
-  mDeadKeyShiftState = VirtualKey::ModifierKeyStateToShiftState(aModKeyState);
+  mActiveDeadKeys.AppendElement(aNativeKey.mOriginalVirtualKeyCode);
+  mDeadKeyShiftStates.AppendElement(
+    VirtualKey::ModifierKeyStateToShiftState(aModKeyState));
 }
 
 void
 KeyboardLayout::DeactivateDeadKeyState()
 {
-  if (mActiveDeadKey < 0) {
+  if (mActiveDeadKeys.IsEmpty()) {
     return;
   }
 
   BYTE kbdState[256];
   memset(kbdState, 0, sizeof(kbdState));
 
-  VirtualKey::FillKbdState(kbdState, mDeadKeyShiftState);
-
-  EnsureDeadKeyActive(false, mActiveDeadKey, kbdState);
-  mActiveDeadKey = -1;
+  // Assume that the last dead key can finish dead key sequence.
+  VirtualKey::FillKbdState(kbdState, mDeadKeyShiftStates.LastElement());
+  EnsureDeadKeyActive(false, mActiveDeadKeys.LastElement(), kbdState);
+  mActiveDeadKeys.Clear();
+  mDeadKeyShiftStates.Clear();
 }
 
 bool
 KeyboardLayout::AddDeadKeyEntry(char16_t aBaseChar,
                                 char16_t aCompositeChar,
                                 DeadKeyEntry* aDeadKeyArray,
                                 uint32_t aEntries)
 {
--- a/widget/windows/KeyboardLayout.h
+++ b/widget/windows/KeyboardLayout.h
@@ -689,17 +689,17 @@ public:
   bool IsDeadKey(uint8_t aVirtualKey,
                  const ModifierKeyState& aModKeyState) const;
 
   /**
    * IsInDeadKeySequence() returns true when it's in a dead key sequence.
    * It starts when a dead key is down and ends when another key down causes
    * inactivating the dead key state.
    */
-  bool IsInDeadKeySequence() const { return mActiveDeadKey >= 0; }
+  bool IsInDeadKeySequence() const { return !mActiveDeadKeys.IsEmpty(); }
 
   /**
    * IsSysKey() returns true if aVirtualKey with aModKeyState causes WM_SYSKEY*
    * or WM_SYS*CHAR messages.
    */
   bool IsSysKey(uint8_t aVirtualKey,
                 const ModifierKeyState& aModKeyState) const;
 
@@ -806,18 +806,24 @@ private:
     DeadKeyTableListEntry* next;
     uint8_t data[1];
   };
 
   HKL mKeyboardLayout;
 
   VirtualKey mVirtualKeys[NS_NUM_OF_KEYS];
   DeadKeyTableListEntry* mDeadKeyTableListHead;
-  int32_t mActiveDeadKey;                 // -1 = no active dead-key
-  VirtualKey::ShiftState mDeadKeyShiftState;
+  // When mActiveDeadKeys is empty, it's not in dead key sequence.
+  // Otherwise, it contains virtual keycodes which are pressed in current
+  // dead key sequence.
+  nsTArray<uint8_t> mActiveDeadKeys;
+  // mDeadKeyShiftStates is always same length as mActiveDeadKeys.
+  // This stores shift states at pressing each dead key stored in
+  // mActiveDeadKeys.
+  nsTArray<VirtualKey::ShiftState> mDeadKeyShiftStates;
 
   bool mIsOverridden;
   bool mIsPendingToRestoreKeyboardLayout;
 
   static inline int32_t GetKeyIndex(uint8_t aVirtualKey);
   static int CompareDeadKeyEntries(const void* aArg1, const void* aArg2,
                                    void* aData);
   static bool AddDeadKeyEntry(char16_t aBaseChar, char16_t aCompositeChar,
@@ -879,25 +885,29 @@ private:
   /**
    * See the comment of GetUniCharsAndModifiers() below.
    */
   UniCharsAndModifiers GetUniCharsAndModifiers(
                          uint8_t aVirtualKey,
                          VirtualKey::ShiftState aShiftState) const;
 
   /**
+   * GetDeadUniCharsAndModifiers() returns dead chars which are stored in
+   * current dead key sequence.  So, this is stateful.
+   */
+  UniCharsAndModifiers GetDeadUniCharsAndModifiers() const;
+
+  /**
    * GetCompositeChar() returns a composite character with dead character
-   * caused by aVirtualKeyOfDeadKey and aShiftStateOfDeadKey and a base
-   * character (aBaseChar).
+   * caused by mActiveDeadKeys, mDeadKeyShiftStates and a base character
+   * (aBaseChar).
    * If the combination of the dead character and the base character doesn't
    * cause a composite character, this returns 0.
    */
-  char16_t GetCompositeChar(uint8_t aVirtualKeyOfDeadKey,
-                            VirtualKey::ShiftState aShiftStateOfDeadKey,
-                            char16_t aBaseChar) const;
+  char16_t GetCompositeChar(char16_t aBaseChar) const;
 
   // NativeKey class should access InitNativeKey() directly, but it shouldn't
   // be available outside of NativeKey.  So, let's make NativeKey a friend
   // class of this.
   friend class NativeKey;
 };
 
 class RedirectedKeyDownMessageManager