Merge mozilla-central to inbound. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Thu, 03 May 2018 00:08:38 +0300
changeset 472864 cb245b9a8b2ce1dbe877f85566cf16613d8d9b44
parent 472863 d38f173dfb452786ef70aeef2a5114d27f68c457 (current diff)
parent 472826 2d83e1843241d869a2fc5cf06f96d3af44c70e70 (diff)
child 472865 bbce41cfd87207c5ce28505bbc8e4f7dda3d75a3
push id1728
push userjlund@mozilla.com
push dateMon, 18 Jun 2018 21:12:27 +0000
treeherdermozilla-release@c296fde26f5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound. a=merge CLOSED TREE
dom/interfaces/html/nsIDOMHTMLInputElement.idl
--- a/.eslintignore
+++ b/.eslintignore
@@ -345,18 +345,21 @@ servo/**
 # Remote protocol exclusions
 testing/marionette/atom.js
 testing/marionette/legacyaction.js
 testing/marionette/client
 testing/marionette/doc
 testing/marionette/harness
 
 # other testing/ exclusions
-testing/mochitest/**
 # third party modules
+testing/mochitest/tests/Harness_sanity/**
+testing/mochitest/MochiKit/**
+testing/mochitest/tests/MochiKit-1.4.2/**
+testing/mochitest/tests/SimpleTest/**
 testing/modules/ajv-4.1.1.js
 testing/modules/sinon-2.3.2.js
 # octothorpe used for pref file comment causes parsing error
 testing/mozbase/mozprofile/tests/files/prefs_with_comments.js
 testing/talos/talos/scripts/jszip.min.js
 testing/talos/talos/startup_test/sessionrestore/profile/sessionstore.js
 testing/talos/talos/startup_test/sessionrestore/profile-manywindows/sessionstore.js
 testing/talos/talos/tests/canvasmark/**
--- a/accessible/tests/mochitest/events.js
+++ b/accessible/tests/mochitest/events.js
@@ -1417,17 +1417,17 @@ function closeCombobox(aComboboxID) {
 
 /**
  * Select all invoker.
  */
 function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) {
   this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq);
 
   this.invoke = function synthSelectAll_invoke() {
-    if (this.DOMNode instanceof Ci.nsIDOMHTMLInputElement ||
+    if (ChromeUtils.getClassName(this.DOMNode) === "HTMLInputElement" ||
         this.DOMNode.localName == "textbox") {
       this.DOMNode.select();
 
     } else {
       window.getSelection().selectAllChildren(this.DOMNode);
     }
   };
 
--- a/browser/components/extensions/schemas/windows.json
+++ b/browser/components/extensions/schemas/windows.json
@@ -439,17 +439,17 @@
         "name": "remove",
         "type": "function",
         "description": "Removes (closes) a window, and all the tabs inside it.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "windowId",
-            "minimum": 0
+            "minimum": -2
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": []
           }
         ]
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -212,13 +212,14 @@ skip-if = debug || os == "linux" #Bug 13
 [browser_ext_webNavigation_urlbar_transitions.js]
 [browser_ext_windows.js]
 [browser_ext_windows_create.js]
 tags = fullscreen
 [browser_ext_windows_create_params.js]
 [browser_ext_windows_create_tabId.js]
 [browser_ext_windows_create_url.js]
 [browser_ext_windows_events.js]
+[browser_ext_windows_remove.js]
 [browser_ext_windows_size.js]
 skip-if = os == 'mac' # Fails when windows are randomly opened in fullscreen mode
 [browser_ext_windows_update.js]
 tags = fullscreen
 [browser_ext_contentscript_animate.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_remove.js
@@ -0,0 +1,45 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWindowRemove() {
+  let extension = ExtensionTestUtils.loadExtension({
+    async background() {
+      async function closeWindow(id) {
+        let window = await browser.windows.get(id);
+        return new Promise(function(resolve) {
+          browser.windows.onRemoved.addListener(async function listener(windowId) {
+            browser.windows.onRemoved.removeListener(listener);
+            await browser.test.assertEq(windowId, window.id, "The right window was closed");
+            await browser.test.assertRejects(
+              browser.windows.get(windowId),
+              new RegExp(`Invalid window ID: ${windowId}`),
+              "The window was really closed.");
+            resolve();
+          });
+          browser.windows.remove(id);
+        });
+      }
+
+      browser.test.log("Create a new window and close it by its ID");
+      let newWindow = await browser.windows.create();
+      await closeWindow(newWindow.id);
+
+      browser.test.log("Create a new window and close it by WINDOW_ID_CURRENT");
+      await browser.windows.create();
+      await closeWindow(browser.windows.WINDOW_ID_CURRENT);
+
+      browser.test.log("Assert failure for bad parameter.");
+      await browser.test.assertThrows(
+        () => browser.windows.remove(-3),
+        /-3 is too small \(must be at least -2\)/,
+        "Invalid windowId throws");
+
+      browser.test.notifyPass("window-remove");
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("window-remove");
+  await extension.unload();
+});
--- a/browser/components/payments/content/paymentDialogFrameScript.js
+++ b/browser/components/payments/content/paymentDialogFrameScript.js
@@ -60,26 +60,34 @@ let PaymentFrameScript = {
       });
     }
   },
 
   /**
    * Expose privileged utility functions to the unprivileged page.
    */
   exposeUtilityFunctions() {
+    let waivedContent = Cu.waiveXrays(content);
     let PaymentDialogUtils = {
+      DEFAULT_REGION: FormAutofillUtils.DEFAULT_REGION,
+      supportedCountries: FormAutofillUtils.supportedCountries,
+
       getAddressLabel(address) {
         return FormAutofillUtils.getAddressLabel(address);
       },
 
       isCCNumber(value) {
         return FormAutofillUtils.isCCNumber(value);
       },
+
+      getFormFormat(country) {
+        let format = FormAutofillUtils.getFormFormat(country);
+        return Cu.cloneInto(format, waivedContent);
+      },
     };
-    let waivedContent = Cu.waiveXrays(content);
     waivedContent.PaymentDialogUtils = Cu.cloneInto(PaymentDialogUtils, waivedContent, {
       cloneFunctions: true,
     });
   },
 
   sendToChrome({detail}) {
     let {messageType} = detail;
     if (messageType == "initializeRequest") {
--- a/browser/components/payments/jar.mn
+++ b/browser/components/payments/jar.mn
@@ -14,13 +14,14 @@ browser.jar:
     res/payments/components/                          (res/components/*.css)
     res/payments/components/                          (res/components/*.js)
     res/payments/containers/                          (res/containers/*.js)
     res/payments/containers/                          (res/containers/*.css)
     res/payments/debugging.css                        (res/debugging.css)
     res/payments/debugging.html                       (res/debugging.html)
     res/payments/debugging.js                         (res/debugging.js)
     res/payments/formautofill/autofillEditForms.js    (../../../browser/extensions/formautofill/content/autofillEditForms.js)
+    res/payments/formautofill/editAddress.xhtml       (../../../browser/extensions/formautofill/content/editAddress.xhtml)
     res/payments/formautofill/editCreditCard.xhtml    (../../../browser/extensions/formautofill/content/editCreditCard.xhtml)
     res/payments/unprivileged-fallbacks.js            (res/unprivileged-fallbacks.js)
     res/payments/mixins/                              (res/mixins/*.js)
     res/payments/PaymentsStore.js                     (res/PaymentsStore.js)
     res/payments/vendor/                              (res/vendor/*)
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/address-form.js
@@ -0,0 +1,154 @@
+/* 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/. */
+
+/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <address-form></address-form>
+ *
+ * XXX: Bug 1446164 - This form isn't localized when used via this custom element
+ * as it will be much easier to share the logic once we switch to Fluent.
+ */
+
+export default class AddressForm extends PaymentStateSubscriberMixin(HTMLElement) {
+  constructor() {
+    super();
+
+    this.pageTitle = document.createElement("h1");
+    this.genericErrorText = document.createElement("div");
+
+    this.backButton = document.createElement("button");
+    this.backButton.addEventListener("click", this);
+
+    this.saveButton = document.createElement("button");
+    this.saveButton.addEventListener("click", this);
+
+    // The markup is shared with form autofill preferences.
+    let url = "formautofill/editAddress.xhtml";
+    this.promiseReady = this._fetchMarkup(url).then(doc => {
+      this.form = doc.getElementById("form");
+      return this.form;
+    });
+  }
+
+  _fetchMarkup(url) {
+    return new Promise((resolve, reject) => {
+      let xhr = new XMLHttpRequest();
+      xhr.responseType = "document";
+      xhr.addEventListener("error", reject);
+      xhr.addEventListener("load", evt => {
+        resolve(xhr.response);
+      });
+      xhr.open("GET", url);
+      xhr.send();
+    });
+  }
+
+  connectedCallback() {
+    this.promiseReady.then(form => {
+      this.appendChild(this.pageTitle);
+      this.appendChild(form);
+
+      let record = {};
+      this.formHandler = new EditAddress({
+        form,
+      }, record, {
+        DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
+        getFormFormat: PaymentDialogUtils.getFormFormat,
+        supportedCountries: PaymentDialogUtils.supportedCountries,
+      });
+
+      this.appendChild(this.genericErrorText);
+      this.appendChild(this.backButton);
+      this.appendChild(this.saveButton);
+      // Only call the connected super callback(s) once our markup is fully
+      // connected, including the shared form fetched asynchronously.
+      super.connectedCallback();
+    });
+  }
+
+  render(state) {
+    this.backButton.textContent = this.dataset.backButtonLabel;
+    this.saveButton.textContent = this.dataset.saveButtonLabel;
+
+    let record = {};
+    let {
+      page,
+      savedAddresses,
+    } = state;
+
+    this.pageTitle.textContent = page.title;
+    this.genericErrorText.textContent = page.error;
+
+    let editing = !!page.guid;
+
+    // If an address is selected we want to edit it.
+    if (editing) {
+      record = savedAddresses[page.guid];
+      if (!record) {
+        throw new Error("Trying to edit a non-existing address: " + page.guid);
+      }
+    }
+
+    this.formHandler.loadRecord(record);
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "click": {
+        this.onClick(event);
+        break;
+      }
+    }
+  }
+
+  onClick(evt) {
+    switch (evt.target) {
+      case this.backButton: {
+        this.requestStore.setState({
+          page: {
+            id: "payment-summary",
+          },
+        });
+        break;
+      }
+      case this.saveButton: {
+        this.saveRecord();
+        break;
+      }
+      default: {
+        throw new Error("Unexpected click target");
+      }
+    }
+  }
+
+  saveRecord() {
+    let record = this.formHandler.buildFormObject();
+    let {
+      page,
+      selectedStateKey,
+    } = this.requestStore.getState();
+
+    paymentRequest.updateAutofillRecord("addresses", record, page.guid, {
+      errorStateChange: {
+        page: {
+          id: "address-page",
+          error: this.dataset.errorGenericSave,
+        },
+      },
+      preserveOldProperties: true,
+      selectedStateKey,
+      successStateChange: {
+        page: {
+          id: "payment-summary",
+        },
+      },
+    });
+  }
+}
+
+customElements.define("address-form", AddressForm);
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -3,33 +3,44 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 import AddressOption from "../components/address-option.js";
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import RichSelect from "../components/rich-select.js";
 
 /**
  * <address-picker></address-picker>
- * Container around <rich-select> (eventually providing add/edit links) with
+ * Container around add/edit links and <rich-select> with
  * <address-option> listening to savedAddresses.
  */
 
 export default class AddressPicker extends PaymentStateSubscriberMixin(HTMLElement) {
   static get observedAttributes() {
     return ["address-fields"];
   }
 
   constructor() {
     super();
     this.dropdown = new RichSelect();
     this.dropdown.addEventListener("change", this);
+    this.addLink = document.createElement("a");
+    this.addLink.href = "javascript:void(0)";
+    this.addLink.textContent = this.dataset.addLinkLabel;
+    this.addLink.addEventListener("click", this);
+    this.editLink = document.createElement("a");
+    this.editLink.href = "javascript:void(0)";
+    this.editLink.textContent = this.dataset.editLinkLabel;
+    this.editLink.addEventListener("click", this);
   }
 
   connectedCallback() {
     this.appendChild(this.dropdown);
+    this.appendChild(this.addLink);
+    this.append(" ");
+    this.appendChild(this.editLink);
     super.connectedCallback();
   }
 
   attributeChangedCallback(name, oldValue, newValue) {
     if (oldValue !== newValue) {
       this.render(this.requestStore.getState());
     }
   }
@@ -129,23 +140,55 @@ export default class AddressPicker exten
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "change": {
         this.onChange(event);
         break;
       }
+      case "click": {
+        this.onClick(event);
+      }
     }
   }
 
   onChange(event) {
     let select = event.target;
     let selectedKey = this.selectedStateKey;
     if (selectedKey) {
       this.requestStore.setState({
         [selectedKey]: select.selectedOption && select.selectedOption.guid,
       });
     }
   }
+
+  onClick({target}) {
+    let nextState = {
+      page: {
+        id: "address-page",
+        selectedStateKey: this.selectedStateKey,
+      },
+    };
+
+    switch (target) {
+      case this.addLink: {
+        nextState.page.guid = null;
+        nextState.page.title = this.dataset.addAddressTitle;
+        break;
+      }
+      case this.editLink: {
+        let state = this.requestStore.getState();
+        let selectedAddressGUID = state[this.selectedStateKey];
+        nextState.page.guid = selectedAddressGUID;
+        nextState.page.title = this.dataset.editAddressTitle;
+        break;
+      }
+      default: {
+        throw new Error("Unexpected onClick");
+      }
+    }
+
+    this.requestStore.setState(nextState);
+  }
 }
 
 customElements.define("address-picker", AddressPicker);
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -15,16 +15,17 @@ import paymentRequest from "../paymentRe
  * XXX: Bug 1446164 - This form isn't localized when used via this custom element
  * as it will be much easier to share the logic once we switch to Fluent.
  */
 
 export default class BasicCardForm extends PaymentStateSubscriberMixin(HTMLElement) {
   constructor() {
     super();
 
+    this.pageTitle = document.createElement("h1");
     this.genericErrorText = document.createElement("div");
 
     this.backButton = document.createElement("button");
     this.backButton.addEventListener("click", this);
 
     this.saveButton = document.createElement("button");
     this.saveButton.addEventListener("click", this);
 
@@ -48,16 +49,17 @@ export default class BasicCardForm exten
       });
       xhr.open("GET", url);
       xhr.send();
     });
   }
 
   connectedCallback() {
     this.promiseReady.then(form => {
+      this.appendChild(this.pageTitle);
       this.appendChild(form);
 
       let record = {};
       let addresses = [];
       this.formHandler = new EditCreditCard({
         form,
       }, record, addresses, {
         isCCNumber: PaymentDialogUtils.isCCNumber,
@@ -70,26 +72,28 @@ export default class BasicCardForm exten
       this.appendChild(this.saveButton);
       // Only call the connected super callback(s) once our markup is fully
       // connected, including the shared form fetched asynchronously.
       super.connectedCallback();
     });
   }
 
   render(state) {
+    let {
+      page,
+      savedAddresses,
+      selectedShippingAddress,
+    } = state;
+
+    this.pageTitle.textContent = page.title;
     this.backButton.textContent = this.dataset.backButtonLabel;
     this.saveButton.textContent = this.dataset.saveButtonLabel;
     this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
 
     let record = {};
-    let {
-      page,
-      savedAddresses,
-      selectedShippingAddress,
-    } = state;
     let basicCards = paymentRequest.getBasicCards(state);
 
     this.genericErrorText.textContent = page.error;
 
     let editing = !!page.guid;
     this.form.querySelector("#cc-number").disabled = editing;
 
     // If a card is selected we want to edit it.
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -4,16 +4,17 @@
 
 import "../vendor/custom-elements.min.js";
 
 import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
 import paymentRequest from "../paymentRequest.js";
 
 import "../components/currency-amount.js";
 import "./address-picker.js";
+import "./address-form.js";
 import "./basic-card-form.js";
 import "./order-details.js";
 import "./payment-method-picker.js";
 import "./shipping-option-picker.js";
 
 /* import-globals-from ../unprivileged-fallbacks.js */
 
 /**
@@ -39,16 +40,17 @@ export default class PaymentDialog exten
 
     this._viewAllButton = contents.querySelector("#view-all");
     this._viewAllButton.addEventListener("click", this);
 
     this._mainContainer = contents.getElementById("main-container");
     this._orderDetailsOverlay = contents.querySelector("#order-details-overlay");
 
     this._shippingTypeLabel = contents.querySelector("#shipping-type-label");
+    this._shippingAddressPicker = contents.querySelector("address-picker.shipping-related");
     this._shippingRelatedEls = contents.querySelectorAll(".shipping-related");
     this._payerRelatedEls = contents.querySelectorAll(".payer-related");
     this._payerAddressPicker = contents.querySelector("address-picker.payer-related");
 
     this._errorText = contents.querySelector("#error-text");
 
     this._disabledOverlay = contents.getElementById("disabled-overlay");
 
@@ -229,16 +231,22 @@ export default class PaymentDialog exten
     this._cachedState.selectedShippingOption = state.selectedShippingOption;
   }
 
   render(state) {
     let request = state.request;
     let paymentDetails = request.paymentDetails;
     this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
 
+    let shippingType = state.request.paymentOptions.shippingType || "shipping";
+    this._shippingAddressPicker.dataset.addAddressTitle =
+      this.dataset[shippingType + "AddressTitleAdd"];
+    this._shippingAddressPicker.dataset.editAddressTitle =
+      this.dataset[shippingType + "AddressTitleEdit"];
+
     let totalItem = paymentRequest.getTotalItem(state);
     let totalAmountEl = this.querySelector("#total > currency-amount");
     totalAmountEl.value = totalItem.amount.value;
     totalAmountEl.currency = totalItem.amount.currency;
 
     this._orderDetailsOverlay.hidden = !state.orderDetailsShowing;
     this._errorText.textContent = paymentDetails.error;
 
@@ -263,18 +271,19 @@ export default class PaymentDialog exten
       }
       if (paymentOptions.requestPayerPhone) {
         fieldNames.add("tel");
       }
       this._payerAddressPicker.setAttribute("address-fields", [...fieldNames].join(" "));
     } else {
       this._payerAddressPicker.removeAttribute("address-fields");
     }
+    this._payerAddressPicker.dataset.addAddressTitle = this.dataset.payerTitleAdd;
+    this._payerAddressPicker.dataset.editAddressTitle = this.dataset.payerTitleEdit;
 
-    let shippingType = paymentOptions.shippingType || "shipping";
     this._shippingTypeLabel.querySelector("label").textContent =
       this._shippingTypeLabel.dataset[shippingType + "AddressLabel"];
 
     this._renderPayButton(state);
 
     for (let page of this._mainContainer.querySelectorAll(":scope > .page")) {
       page.hidden = state.page.id != page.id;
     }
--- a/browser/components/payments/res/containers/payment-method-picker.js
+++ b/browser/components/payments/res/containers/payment-method-picker.js
@@ -131,22 +131,24 @@ export default class PaymentMethodPicker
       page: {
         id: "basic-card-page",
       },
     };
 
     switch (target) {
       case this.addLink: {
         nextState.page.guid = null;
+        nextState.page.title = this.dataset.addBasicCardTitle;
         break;
       }
       case this.editLink: {
         let state = this.requestStore.getState();
         let selectedPaymentCardGUID = state[this.selectedStateKey];
         nextState.page.guid = selectedPaymentCardGUID;
+        nextState.page.title = this.dataset.editBasicCardTitle;
         break;
       }
       default: {
         throw new Error("Unexpected onClick");
       }
     }
 
     this.requestStore.setState(nextState);
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -8,31 +8,48 @@
 
   <!ENTITY viewAllItems               "View All Items">
   <!ENTITY paymentSummaryTitle        "Your Payment">
   <!ENTITY shippingAddressLabel       "Shipping Address">
   <!ENTITY deliveryAddressLabel       "Delivery Address">
   <!ENTITY pickupAddressLabel         "Pickup Address">
   <!ENTITY shippingOptionsLabel       "Shipping Options">
   <!ENTITY paymentMethodsLabel        "Payment Method">
+  <!ENTITY address.addLink.label      "Add">
+  <!ENTITY address.editLink.label     "Edit">
   <!ENTITY basicCard.addLink.label    "Add">
   <!ENTITY basicCard.editLink.label   "Edit">
+  <!ENTITY payer.addLink.label        "Add">
+  <!ENTITY payer.editLink.label       "Edit">
+  <!ENTITY shippingAddress.addPage.title  "Add Shipping Address">
+  <!ENTITY shippingAddress.editPage.title "Edit Shipping Address">
+  <!ENTITY deliveryAddress.addPage.title  "Add Delivery Address">
+  <!ENTITY deliveryAddress.editPage.title "Edit Delivery Address">
+  <!ENTITY pickupAddress.addPage.title    "Add Pickup Address">
+  <!ENTITY pickupAddress.editPage.title   "Edit Pickup Address">
+  <!ENTITY basicCard.addPage.title    "Add Credit Card">
+  <!ENTITY basicCard.editPage.title   "Edit Credit Card">
+  <!ENTITY payer.addPage.title        "Add Payer Contact">
+  <!ENTITY payer.editPage.title       "Edit Payer Contact">
   <!ENTITY payerLabel                 "Contact Information">
   <!ENTITY cancelPaymentButton.label   "Cancel">
   <!ENTITY approvePaymentButton.label  "Pay">
   <!ENTITY processingPaymentButton.label "Processing">
   <!ENTITY successPaymentButton.label    "Done">
   <!ENTITY failPaymentButton.label       "Fail">
   <!ENTITY unknownPaymentButton.label    "Unknown">
   <!ENTITY orderDetailsLabel          "Order Details">
   <!ENTITY orderTotalLabel            "Total">
   <!ENTITY basicCardPage.error.genericSave    "There was an error saving the payment card.">
   <!ENTITY basicCardPage.backButton.label     "Back">
   <!ENTITY basicCardPage.saveButton.label     "Save">
   <!ENTITY basicCardPage.persistCheckbox.label     "Save credit card to Firefox (Security code will not be saved)">
+  <!ENTITY addressPage.error.genericSave      "There was an error saving the address.">
+  <!ENTITY addressPage.backButton.label       "Back">
+  <!ENTITY addressPage.saveButton.label       "Save">
 ]>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <title>&paymentSummaryTitle;</title>
 
   <!-- chrome: is needed for global.dtd -->
   <meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
 
@@ -70,29 +87,36 @@
         <section>
           <div id="error-text"></div>
 
           <div class="shipping-related"
                id="shipping-type-label"
                data-shipping-address-label="&shippingAddressLabel;"
                data-delivery-address-label="&deliveryAddressLabel;"
                data-pickup-address-label="&pickupAddressLabel;"><label></label></div>
-          <address-picker class="shipping-related" selected-state-key="selectedShippingAddress"></address-picker>
+          <address-picker class="shipping-related"
+                          data-add-link-label="&address.addLink.label;"
+                          data-edit-link-label="&address.editLink.label;"
+                          selected-state-key="selectedShippingAddress"></address-picker>
 
           <div class="shipping-related"><label>&shippingOptionsLabel;</label></div>
           <shipping-option-picker class="shipping-related"></shipping-option-picker>
 
           <div><label>&paymentMethodsLabel;</label></div>
           <payment-method-picker selected-state-key="selectedPaymentCard"
+                                 data-add-basic-card-title="&basicCard.addPage.title;"
+                                 data-edit-basic-card-title="&basicCard.editPage.title;"
                                  data-add-link-label="&basicCard.addLink.label;"
                                  data-edit-link-label="&basicCard.editLink.label;">
           </payment-method-picker>
 
           <div class="payer-related"><label>&payerLabel;</label></div>
           <address-picker class="payer-related"
+                          data-add-link-label="&payer.addLink.label;"
+                          data-edit-link-label="&payer.editLink.label;"
                           selected-state-key="selectedPayerAddress"></address-picker>
           <div id="error-text"></div>
         </section>
 
         <footer id="controls-container">
           <button id="cancel">&cancelPaymentButton.label;</button>
           <button id="pay"
                   data-initial-label="&approvePaymentButton.label;"
@@ -109,16 +133,23 @@
 
       <basic-card-form id="basic-card-page"
                        class="page"
                        data-error-generic-save="&basicCardPage.error.genericSave;"
                        data-back-button-label="&basicCardPage.backButton.label;"
                        data-save-button-label="&basicCardPage.saveButton.label;"
                        data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
                        hidden="hidden"></basic-card-form>
+
+      <address-form id="address-page"
+                    class="page"
+                    data-error-generic-save="&addressPage.error.genericSave;"
+                    data-back-button-label="&addressPage.backButton.label;"
+                    data-save-button-label="&addressPage.saveButton.label;"
+                    hidden="hidden"></address-form>
     </div>
 
     <div id="disabled-overlay" hidden="hidden">
       <!-- overlay to prevent changes while waiting for a response from the merchant -->
     </div>
   </template>
 
   <template id="order-details-template">
@@ -130,11 +161,18 @@
       <currency-amount></currency-amount>
     </div>
   </template>
 </head>
 <body dir="&locale.dir;">
   <iframe id="debugging-console"
           hidden="hidden"
           height="400"></iframe>
-  <payment-dialog></payment-dialog>
+  <payment-dialog data-shipping-address-title-add="&shippingAddress.addPage.title;"
+                  data-shipping-address-title-edit="&shippingAddress.editPage.title;"
+                  data-delivery-address-title-add="&deliveryAddress.addPage.title;"
+                  data-delivery-address-title-edit="&deliveryAddress.editPage.title;"
+                  data-pickup-address-title-add="&pickupAddress.addPage.title;"
+                  data-pickup-address-title-edit="&pickupAddress.editPage.title;"
+                  data-payer-title-add="&payer.addPage.title;"
+                  data-payer-title-edit="&payer.editPage.title;"></payment-dialog>
 </body>
 </html>
--- a/browser/components/payments/res/unprivileged-fallbacks.js
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -23,9 +23,25 @@ var log = {
 
 var PaymentDialogUtils = {
   getAddressLabel(address) {
     return `${address.name} (${address.guid})`;
   },
   isCCNumber(str) {
     return str.length > 0;
   },
+  DEFAULT_REGION: "US",
+  supportedCountries: ["US", "CA"],
+  getFormFormat(country) {
+    return {
+      "addressLevel1Label": country == "US" ? "state" : "province",
+      "postalCodeLabel": country == "US" ? "zip" : "postalCode",
+      "fieldsOrder": [
+        {fieldId: "name", newLine: true},
+        {fieldId: "organization", newLine: true},
+        {fieldId: "street-address", newLine: true},
+        {fieldId: "address-level2"},
+        {fieldId: "address-level1"},
+        {fieldId: "postal-code"},
+      ],
+    };
+  },
 };
--- a/browser/components/payments/test/browser/browser.ini
+++ b/browser/components/payments/test/browser/browser.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 head = head.js
 prefs =
   dom.payments.request.enabled=true
 skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
 support-files =
   blank_page.html
 
+[browser_address_edit.js]
 [browser_card_edit.js]
 [browser_change_shipping.js]
 [browser_host_name.js]
 [browser_profile_storage.js]
 [browser_request_serialization.js]
 [browser_request_shipping.js]
 [browser_request_summary.js]
 uses-unsafe-cpows = true
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/test/browser/browser_address_edit.js
@@ -0,0 +1,167 @@
+/* eslint-disable no-shadow */
+
+"use strict";
+
+add_task(async function test_add_link() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    let {win, frame} =
+      await setupPaymentDialog(browser, {
+        methodData: [PTU.MethodData.basicCard],
+        details: PTU.Details.twoShippingOptions,
+        options: PTU.Options.requestShippingOption,
+        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+      }
+    );
+
+    let shippingAddressChangePromise = ContentTask.spawn(browser, {
+      eventName: "shippingaddresschange",
+    }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
+
+    const EXPECTED_ADDRESS = {
+      "given-name": "Jared",
+      "family-name": "Wein",
+      "organization": "Mozilla",
+      "street-address": "404 Internet Lane",
+      "address-level2": "Firefoxity City",
+      "address-level1": "CA",
+      "postal-code": "31337",
+      "country": "US",
+      "tel": "+15555551212",
+      "email": "test@example.com",
+    };
+
+    await spawnPaymentDialogTask(frame, async (address) => {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      let addLink = content.document.querySelector("address-picker a");
+      is(addLink.textContent, "Add", "Add link text");
+
+      addLink.click();
+
+      let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" && !state.page.guid;
+      }, "Check add page state");
+
+      let title = content.document.querySelector("address-form h1");
+      is(title.textContent, "Add Shipping Address", "Page title should be set");
+
+      info("filling fields");
+      for (let [key, val] of Object.entries(address)) {
+        let field = content.document.getElementById(key);
+        if (!field) {
+          ok(false, `${key} field not found`);
+        }
+        field.value = val;
+        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+      }
+
+      content.document.querySelector("address-form button:last-of-type").click();
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return Object.keys(state.savedAddresses).length == 1;
+      }, "Check address was added");
+
+      let addressGUIDs = Object.keys(state.savedAddresses);
+      is(addressGUIDs.length, 1, "Check there is one address");
+      let savedAddress = state.savedAddresses[addressGUIDs[0]];
+      for (let [key, val] of Object.entries(address)) {
+        is(savedAddress[key], val, "Check " + key);
+      }
+
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "payment-summary";
+      }, "Switched back to payment-summary");
+    }, EXPECTED_ADDRESS);
+
+    await shippingAddressChangePromise;
+    info("got shippingaddresschange event");
+
+    info("clicking cancel");
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+  });
+});
+
+add_task(async function test_edit_link() {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: BLANK_PAGE_URL,
+  }, async browser => {
+    let {win, frame} =
+      await setupPaymentDialog(browser, {
+        methodData: [PTU.MethodData.basicCard],
+        details: PTU.Details.twoShippingOptions,
+        options: PTU.Options.requestShippingOption,
+        merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+      }
+    );
+
+    let shippingAddressChangePromise = ContentTask.spawn(browser, {
+      eventName: "shippingaddresschange",
+    }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
+
+    const EXPECTED_ADDRESS = {
+      "given-name": "Jaws",
+      "family-name": "swaJ",
+      "organization": "aliizoM",
+    };
+
+    await spawnPaymentDialogTask(frame, async (address) => {
+      let {
+        PaymentTestUtils: PTU,
+      } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+      let editLink = content.document.querySelector("address-picker a:nth-of-type(2)");
+      is(editLink.textContent, "Edit", "Edit link text");
+
+      editLink.click();
+
+      let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "address-page" && !!state.page.guid;
+      }, "Check edit page state");
+
+      let title = content.document.querySelector("address-form h1");
+      is(title.textContent, "Edit Shipping Address", "Page title should be set");
+
+      info("overwriting field values");
+      for (let [key, val] of Object.entries(address)) {
+        let field = content.document.getElementById(key);
+        field.value = val;
+        ok(!field.disabled, `Field #${key} shouldn't be disabled`);
+      }
+
+      content.document.querySelector("address-form button:last-of-type").click();
+
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        let addresses = Object.entries(state.savedAddresses);
+        return addresses.length == 1 &&
+               addresses[0][1]["given-name"] == address["given-name"];
+      }, "Check address was edited");
+
+      let addressGUIDs = Object.keys(state.savedAddresses);
+      is(addressGUIDs.length, 1, "Check there is still one address");
+      let savedAddress = state.savedAddresses[addressGUIDs[0]];
+      for (let [key, val] of Object.entries(address)) {
+        is(savedAddress[key], val, "Check updated " + key);
+      }
+
+      state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+        return state.page.id == "payment-summary";
+      }, "Switched back to payment-summary");
+    }, EXPECTED_ADDRESS);
+
+    await shippingAddressChangePromise;
+    info("got shippingaddresschange event");
+
+    info("clicking cancel");
+    spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
+
+    await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+  });
+  await cleanupFormAutofillStorage();
+});
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -14,18 +14,20 @@ add_task(async function test_add_link() 
 
     let addLink = content.document.querySelector("payment-method-picker a");
     is(addLink.textContent, "Add", "Add link text");
 
     addLink.click();
 
     let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "basic-card-page" && !state.page.guid;
-    },
-                                                          "Check add page state");
+    }, "Check add page state");
+
+    let title = content.document.querySelector("basic-card-form h1");
+    is(title.textContent, "Add Credit Card", "Add title should be set");
 
     ok(!state.isPrivate,
        "isPrivate flag is not set when paymentrequest is shown from a non-private session");
     let persistInput = content.document.querySelector("basic-card-form labelled-checkbox");
     ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
 
     let year = (new Date()).getFullYear();
     let card = {
@@ -41,31 +43,29 @@ add_task(async function test_add_link() 
       field.value = val;
       ok(!field.disabled, `Field #${key} shouldn't be disabled`);
     }
 
     content.document.querySelector("basic-card-form button:last-of-type").click();
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return Object.keys(state.savedBasicCards).length == 1;
-    },
-                                                      "Check card was added");
+    }, "Check card was added");
 
     let cardGUIDs = Object.keys(state.savedBasicCards);
     is(cardGUIDs.length, 1, "Check there is one card");
     let savedCard = state.savedBasicCards[cardGUIDs[0]];
     card["cc-number"] = "************1111"; // Card should be masked
     for (let [key, val] of Object.entries(card)) {
       is(savedCard[key], val, "Check " + key);
     }
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "payment-summary";
-    },
-                                                      "Switched back to payment-summary");
+    }, "Switched back to payment-summary");
   }, args);
 });
 
 add_task(async function test_edit_link() {
   const args = {
     methodData: [PTU.MethodData.basicCard],
     details: PTU.Details.total60USD,
   };
@@ -76,18 +76,20 @@ add_task(async function test_edit_link()
 
     let editLink = content.document.querySelector("payment-method-picker a:nth-of-type(2)");
     is(editLink.textContent, "Edit", "Edit link text");
 
     editLink.click();
 
     let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "basic-card-page" && !!state.page.guid;
-    },
-                                                          "Check edit page state");
+    }, "Check edit page state");
+
+    let title = content.document.querySelector("basic-card-form h1");
+    is(title.textContent, "Edit Credit Card", "Edit title should be set");
 
     let nextYear = (new Date()).getFullYear() + 1;
     let card = {
       // cc-number cannot be modified
       "cc-name": "A. Nonymous",
       "cc-exp-month": 3,
       "cc-exp-year": nextYear,
     };
@@ -99,31 +101,29 @@ add_task(async function test_edit_link()
       ok(!field.disabled, `Field #${key} shouldn't be disabled`);
     }
     ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled");
 
     content.document.querySelector("basic-card-form button:last-of-type").click();
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return Object.keys(state.savedBasicCards).length == 1;
-    },
-                                                      "Check card was added");
+    }, "Check card was added");
 
     let cardGUIDs = Object.keys(state.savedBasicCards);
     is(cardGUIDs.length, 1, "Check there is still one card");
     let savedCard = state.savedBasicCards[cardGUIDs[0]];
     is(savedCard["cc-number"], "************1111", "Card number should be masked and unmodified.");
     for (let [key, val] of Object.entries(card)) {
       is(savedCard[key], val, "Check updated " + key);
     }
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "payment-summary";
-    },
-                                                      "Switched back to payment-summary");
+    }, "Switched back to payment-summary");
   }, args);
 });
 
 add_task(async function test_private_persist_defaults() {
   const args = {
     methodData: [PTU.MethodData.basicCard],
     details: PTU.Details.total60USD,
   };
--- a/browser/components/payments/test/mochitest/formautofill/mochitest.ini
+++ b/browser/components/payments/test/mochitest/formautofill/mochitest.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 # This manifest mostly exists so that the support-files below can be referenced
 # from a relative path of formautofill/* from the tests in the above directory
 # to resemble the layout in the shipped JAR file.
 support-files =
    ../../../../../../browser/extensions/formautofill/content/editCreditCard.xhtml
+   ../../../../../../browser/extensions/formautofill/content/editAddress.xhtml
 
 [test_editCreditCard.html]
--- a/browser/components/payments/test/mochitest/mochitest.ini
+++ b/browser/components/payments/test/mochitest/mochitest.ini
@@ -1,19 +1,21 @@
 [DEFAULT]
 prefs =
    dom.webcomponents.customelements.enabled=false
 support-files =
+   !/browser/extensions/formautofill/content/editAddress.xhtml
    !/browser/extensions/formautofill/content/editCreditCard.xhtml
    ../../../../../browser/extensions/formautofill/content/autofillEditForms.js
    ../../../../../testing/modules/sinon-2.3.2.js
    ../../res/**
    payments_common.js
 skip-if = !e10s
 
+[test_address_form.html]
 [test_address_picker.html]
 [test_basic_card_form.html]
 [test_currency_amount.html]
 [test_labelled_checkbox.html]
 [test_order_details.html]
 [test_payer_address_picker.html]
 [test_payment_dialog.html]
 [test_payment_details_item.html]
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -0,0 +1,234 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the address-form element
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the address-form element</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script src="sinon-2.3.2.js"></script>
+  <script src="payments_common.js"></script>
+  <script src="../../res/vendor/custom-elements.min.js"></script>
+  <script src="../../res/unprivileged-fallbacks.js"></script>
+  <script src="autofillEditForms.js"></script>
+
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="module">
+/** Test the address-form element **/
+
+/* global sinon */
+/* import-globals-from payments_common.js */
+
+import AddressForm from "../../res/containers/address-form.js";
+
+let display = document.getElementById("display");
+
+function checkAddressForm(customEl, expectedAddress) {
+  const ADDRESS_PROPERTY_NAMES = [
+    "given-name",
+    "family-name",
+    "organization",
+    "street-address",
+    "address-level2",
+    "address-level1",
+    "postal-code",
+    "country",
+    "email",
+    "tel",
+  ];
+  for (let propName of ADDRESS_PROPERTY_NAMES) {
+    let expectedVal = expectedAddress[propName] || "";
+    is(document.getElementById(propName).value,
+       expectedVal.toString(),
+       `Check ${propName}`);
+  }
+}
+
+add_task(async function test_initialState() {
+  let form = new AddressForm();
+  let {page} = form.requestStore.getState();
+  is(page.id, "payment-summary", "Check initial page");
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+  is(page.id, "payment-summary", "Check initial page after appending");
+  form.remove();
+});
+
+add_task(async function test_backButton() {
+  let form = new AddressForm();
+  form.dataset.backButtonLabel = "Back";
+  await form.requestStore.setState({
+    page: {
+      id: "test-page",
+      title: "Sample page title",
+    },
+  });
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let stateChangePromise = promiseStateChange(form.requestStore);
+  is(form.pageTitle.textContent, "Sample page title", "Check label");
+
+  is(form.backButton.textContent, "Back", "Check label");
+  form.backButton.scrollIntoView();
+  synthesizeMouseAtCenter(form.backButton, {});
+
+  let {page} = await stateChangePromise;
+  is(page.id, "payment-summary", "Check initial page after appending");
+
+  form.remove();
+});
+
+add_task(async function test_saveButton() {
+  let form = new AddressForm();
+  form.dataset.saveButtonLabel = "Save";
+  form.dataset.errorGenericSave = "Generic error";
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  form.form.querySelector("#given-name").focus();
+  sendString("Jaws");
+  form.form.querySelector("#family-name").focus();
+  sendString("Swaj");
+  form.form.querySelector("#organization").focus();
+  sendString("Allizom");
+  form.form.querySelector("#street-address").focus();
+  sendString("404 Internet Super Highway");
+  form.form.querySelector("#address-level2").focus();
+  sendString("Firefoxity City");
+  form.form.querySelector("#address-level1").focus();
+  sendString("CA");
+  form.form.querySelector("#postal-code").focus();
+  sendString("00001");
+  form.form.querySelector("#country option[value='US']").selected = true;
+  form.form.querySelector("#email").focus();
+  sendString("test@example.com");
+  form.form.querySelector("#tel").focus();
+  sendString("+15555551212");
+
+  let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
+  is(form.saveButton.textContent, "Save", "Check label");
+  form.saveButton.scrollIntoView();
+  synthesizeMouseAtCenter(form.saveButton, {});
+
+  let details = await messagePromise;
+  is(details.collectionName, "addresses", "Check collectionName");
+  isDeeply(details, {
+    collectionName: "addresses",
+    errorStateChange: {
+      page: {
+        id: "address-page",
+        error: "Generic error",
+      },
+    },
+    guid: undefined,
+    messageType: "updateAutofillRecord",
+    preserveOldProperties: true,
+    record: {
+      "given-name": "Jaws",
+      "family-name": "Swaj",
+      "organization": "Allizom",
+      "street-address": "404 Internet Super Highway",
+      "address-level2": "Firefoxity City",
+      "address-level1": "CA",
+      "postal-code": "00001",
+      "country": "US",
+      "email": "test@example.com",
+      "tel": "+15555551212",
+    },
+    selectedStateKey: undefined,
+    successStateChange: {
+      page: {
+        id: "payment-summary",
+      },
+    },
+  }, "Check event details for the message to chrome");
+  form.remove();
+});
+
+add_task(async function test_genericError() {
+  let form = new AddressForm();
+  await form.requestStore.setState({
+    page: {
+      id: "test-page",
+      error: "Generic Error",
+    },
+  });
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  ok(!isHidden(form.genericErrorText), "Error message should be visible");
+  is(form.genericErrorText.textContent, "Generic Error", "Check error message");
+  form.remove();
+});
+
+add_task(async function test_edit() {
+  let form = new AddressForm();
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let address1 = deepClone(PTU.Addresses.TimBL);
+  address1.guid = "9864798564";
+
+  await form.requestStore.setState({
+    page: {
+      id: "address-page",
+      guid: address1.guid,
+    },
+    savedAddresses: {
+      [address1.guid]: deepClone(address1),
+    },
+  });
+  await asyncElementRendered();
+  checkAddressForm(form, address1);
+
+  info("test change to minimal record");
+  let minimalAddress = {
+    "given-name": address1["given-name"],
+    guid: "9gnjdhen46",
+  };
+  await form.requestStore.setState({
+    page: {
+      id: "address-page",
+      guid: minimalAddress.guid,
+    },
+    savedAddresses: {
+      [minimalAddress.guid]: deepClone(minimalAddress),
+    },
+  });
+  await asyncElementRendered();
+  checkAddressForm(form, minimalAddress);
+
+  info("change to no selected address");
+  await form.requestStore.setState({
+    page: {
+      id: "address-page",
+    },
+  });
+  await asyncElementRendered();
+  checkAddressForm(form, {});
+
+  form.remove();
+});
+</script>
+
+</body>
+</html>
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -64,23 +64,25 @@ add_task(async function test_initialStat
 });
 
 add_task(async function test_backButton() {
   let form = new BasicCardForm();
   form.dataset.backButtonLabel = "Back";
   await form.requestStore.setState({
     page: {
       id: "test-page",
+      title: "Sample page title 2",
     },
   });
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
   let stateChangePromise = promiseStateChange(form.requestStore);
+  is(form.pageTitle.textContent, "Sample page title 2", "Check title");
   is(form.backButton.textContent, "Back", "Check label");
   synthesizeMouseAtCenter(form.backButton, {});
 
   let {page} = await stateChangePromise;
   is(page.id, "payment-summary", "Check initial page after appending");
 
   form.remove();
 });
--- a/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
@@ -57,17 +57,17 @@ function test() {
       Array.forEach(node.options, (aOpt, aIx) =>
         (aOpt.selected = aValue.indexOf(aIx) > -1));
   }
 
   function compareFormValue(aTab, aQuery, aValue) {
     let node = getElementByXPath(aTab, aQuery);
     if (!node)
       return false;
-    if (node instanceof Ci.nsIDOMHTMLInputElement)
+    if (ChromeUtils.getClassName(node) === "HTMLInputElement")
       return aValue == (node.type == "checkbox" || node.type == "radio" ?
                        node.checked : node.value);
     if (ChromeUtils.getClassName(node) === "HTMLTextAreaElement")
       return aValue == node.value;
     if (!node.multiple)
       return aValue == node.selectedIndex;
     return Array.every(node.options, (aOpt, aIx) =>
             (aValue.indexOf(aIx) > -1) == aOpt.selected);
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -598,17 +598,17 @@ var FormAutofillContent = {
 
   onPopupClosed() {
     ProfileAutocomplete._clearProfilePreview();
   },
 
   _markAsAutofillField(field) {
     // Since Form Autofill popup is only for input element, any non-Input
     // element should be excluded here.
-    if (!field || !(field instanceof Ci.nsIDOMHTMLInputElement)) {
+    if (!field || ChromeUtils.getClassName(field) !== "HTMLInputElement") {
       return;
     }
 
     formFillController.markAsAutofillField(field);
   },
 
   _messageManagerFromWindow(win) {
     return win.QueryInterface(Ci.nsIInterfaceRequestor)
--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -265,17 +265,17 @@ class FormAutofillSection {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         continue;
       }
 
       element.previewValue = "";
       let value = profile[fieldDetail.fieldName];
 
-      if (element instanceof Ci.nsIDOMHTMLInputElement && value) {
+      if (ChromeUtils.getClassName(element) === "HTMLInputElement" && value) {
         // For the focused input element, it will be filled with a valid value
         // anyway.
         // For the others, the fields should be only filled when their values
         // are empty.
         let focusedInput = focusedDetail.elementWeakRef.get();
         if (element == focusedInput ||
             (element != focusedInput && !element.value)) {
           element.setUserInput(value);
@@ -374,17 +374,17 @@ class FormAutofillSection {
       let element = fieldDetail.elementWeakRef.get();
       if (!element) {
         log.warn(fieldDetail.fieldName, "is unreachable");
         continue;
       }
 
       // Only reset value for input element.
       if (fieldDetail.state == FIELD_STATES.AUTO_FILLED &&
-          element instanceof Ci.nsIDOMHTMLInputElement) {
+          ChromeUtils.getClassName(element) === "HTMLInputElement") {
         element.setUserInput("");
       }
     }
   }
 
   /**
    * Change the state of a field to correspond with different presentations.
    *
@@ -583,17 +583,17 @@ class FormAutofillAddressSection extends
 
   addressTransformer(profile) {
     if (profile["street-address"]) {
       // "-moz-street-address-one-line" is used by the labels in
       // ProfileAutoCompleteResult.
       profile["-moz-street-address-one-line"] = this._getOneLineStreetAddress(profile["street-address"]);
       let streetAddressDetail = this.getFieldDetailByName("street-address");
       if (streetAddressDetail &&
-          (streetAddressDetail.elementWeakRef.get() instanceof Ci.nsIDOMHTMLInputElement)) {
+          (ChromeUtils.getClassName(streetAddressDetail.elementWeakRef.get()) === "HTMLInputElement")) {
         profile["street-address"] = profile["-moz-street-address-one-line"];
       }
 
       let waitForConcat = [];
       for (let f of ["address-line3", "address-line2", "address-line1"]) {
         waitForConcat.unshift(profile[f]);
         if (this.getFieldDetailByName(f)) {
           if (waitForConcat.length > 1) {
--- a/browser/installer/allowed-dupes.mn
+++ b/browser/installer/allowed-dupes.mn
@@ -139,12 +139,14 @@ res/table-remove-row-active.gif
 res/table-remove-row-hover.gif
 res/table-remove-row.gif
 res/multilocale.txt
 update.locale
 # Aurora branding
 browser/chrome/browser/content/branding/icon128.png
 browser/chrome/devtools/content/framework/dev-edition-promo/dev-edition-logo.png
 # Bug 1451016 - Nightly-only PaymentRequest & Form Autofill code sharing.
+browser/features/formautofill@mozilla.org/chrome/content/editAddress.xhtml
+browser/chrome/browser/res/payments/formautofill/editAddress.xhtml
 browser/features/formautofill@mozilla.org/chrome/content/editCreditCard.xhtml
 browser/chrome/browser/res/payments/formautofill/editCreditCard.xhtml
 browser/features/formautofill@mozilla.org/chrome/content/autofillEditForms.js
 browser/chrome/browser/res/payments/formautofill/autofillEditForms.js
--- a/build.gradle
+++ b/build.gradle
@@ -45,17 +45,17 @@ buildscript {
             }
         }
         // For in tree plugins.
         maven {
             url "file://${gradle.mozconfig.topsrcdir}/mobile/android/gradle/m2repo"
         }
     }
 
-    ext.kotlin_version = '1.1.51'
+    ext.kotlin_version = '1.2.41'
     ext.support_library_version = '23.4.0'
 
     if (gradle.mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
         ext.google_play_services_version = '8.4.0'
     }
 
     dependencies {
         classpath 'com.android.tools.build:gradle:3.0.1'
--- a/devtools/client/application/moz.build
+++ b/devtools/client/application/moz.build
@@ -5,8 +5,10 @@
 DIRS += [
     'src',
 ]
 
 DevToolsModules(
     'application.css',
     'panel.js'
 )
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/devtools/client/application/panel.js
+++ b/devtools/client/application/panel.js
@@ -26,16 +26,17 @@ class ApplicationPanel {
     if (!this.toolbox.target.isRemote) {
       await this.toolbox.target.makeRemote();
     }
 
     await this.panelWin.Application.bootstrap({
       toolbox: this.toolbox,
       panel: this,
     });
+
     this.emit("ready");
     this.isReady = true;
     return this;
   }
 
   destroy() {
     this.panelWin.Application.destroy();
     this.panelWin = null;
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/.eslintrc.js
@@ -0,0 +1,10 @@
+/* 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";
+
+module.exports = {
+  // Extend from the shared list of defined globals for mochitests.
+  "extends": "../../../.eslintrc.mochitests.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/browser.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  head.js
+  service-workers/dynamic-registration.html
+  service-workers/empty-sw.js
+  service-workers/scope-page.html
+  service-workers/simple.html
+  !/devtools/client/shared/test/frame-script-utils.js
+  !/devtools/client/shared/test/shared-head.js
+
+[browser_application_panel_list-several-workers.js]
+[browser_application_panel_list-single-worker.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/browser_application_panel_list-several-workers.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that the application panel can display several service workers applying to the
+ * same domain.
+ */
+
+const SIMPLE_URL = URL_ROOT + "service-workers/simple.html";
+const OTHER_SCOPE_URL = URL_ROOT + "service-workers/scope-page.html";
+
+add_task(async function() {
+  await enableApplicationPanel();
+
+  let { panel, target } = await openNewTabAndApplicationPanel(SIMPLE_URL);
+  let doc = panel.panelWin.document;
+
+  info("Wait until the service worker appears in the application panel");
+  await waitUntil(() => getWorkerContainers(doc).length === 1);
+
+  info("Wait until the unregister button is displayed for the service worker");
+  await waitUntil(() => getWorkerContainers(doc)[0].querySelector(".unregister-button"));
+
+  ok(true, "First service worker registration is displayed");
+
+  info("Navigate to another page for the same domain with another service worker");
+  await navigate(target, OTHER_SCOPE_URL);
+
+  info("Wait until the service worker appears in the application panel");
+  await waitUntil(() => getWorkerContainers(doc).length === 2);
+
+  info("Wait until the unregister button is displayed for the service worker");
+  await waitUntil(() => getWorkerContainers(doc)[1].querySelector(".unregister-button"));
+
+  ok(true, "Second service worker registration is displayed");
+
+  info("Unregister all service workers");
+  while (getWorkerContainers(doc).length > 0) {
+    let count = getWorkerContainers(doc).length;
+    info("Click on the unregister button for the first service worker");
+    getWorkerContainers(doc)[0].querySelector(".unregister-button").click();
+
+    info("Wait until the service worker is removed from the application panel");
+    await waitUntil(() => getWorkerContainers(doc).length === count - 1);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/browser_application_panel_list-single-worker.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TAB_URL = URL_ROOT + "service-workers/dynamic-registration.html";
+
+add_task(async function() {
+  await enableApplicationPanel();
+
+  let { panel, tab } = await openNewTabAndApplicationPanel(TAB_URL);
+  let doc = panel.panelWin.document;
+
+  let isWorkerListEmpty = !!doc.querySelector(".worker-list-empty");
+  ok(isWorkerListEmpty, "No Service Worker displayed");
+
+  info("Register a service worker in the page.");
+  await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
+    content.wrappedJSObject.registerServiceWorker();
+  });
+
+  info("Wait until the service worker appears in the application panel");
+  await waitUntil(() => getWorkerContainers(doc).length > 0);
+
+  let workerContainer = getWorkerContainers(doc)[0];
+
+  info("Wait until the unregister button is displayed for the service worker");
+  await waitUntil(() => workerContainer.querySelector(".unregister-button"));
+
+  let scopeEl = workerContainer.querySelector(".service-worker-scope");
+  let expectedScope = "example.com/browser/devtools/client/application/test/" +
+                      "service-workers/";
+  ok(scopeEl.textContent.startsWith(expectedScope),
+    "Service worker has the expected scope");
+
+  info("Unregister the service worker");
+  await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
+    let registration = await content.wrappedJSObject.sw;
+    registration.unregister();
+  });
+
+  info("Wait until the service worker is removed from the application panel");
+  await waitUntil(() => getWorkerContainers(doc).length === 0);
+});
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/head.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../shared/test/shared-head.js */
+
+"use strict";
+
+// Load the shared-head file first.
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+  this);
+
+/**
+ * Set all preferences needed to enable service worker debugging and testing.
+ */
+async function enableServiceWorkerDebugging() {
+  // Enable service workers.
+  await pushPref("dom.serviceWorkers.enabled", true);
+  // Accept workers from mochitest's http.
+  await pushPref("dom.serviceWorkers.testing.enabled", true);
+  // Force single content process, see Bug 1231208 for the SW refactor that should enable
+  // SW debugging in multi-e10s.
+  await pushPref("dom.ipc.processCount", 1);
+
+  // Wait for dom.ipc.processCount to be updated before releasing processes.
+  Services.ppmm.releaseCachedProcesses();
+}
+
+async function enableApplicationPanel() {
+  // Enable all preferences related to service worker debugging.
+  await enableServiceWorkerDebugging();
+
+  // Enable application panel in DevTools.
+  await pushPref("devtools.application.enabled", true);
+}
+
+function getWorkerContainers(doc) {
+  return doc.querySelectorAll(".service-worker-container");
+}
+
+function navigate(target, url, waitForTargetEvent = "navigate") {
+  executeSoon(() => target.activeTab.navigateTo(url));
+  return once(target, waitForTargetEvent);
+}
+
+async function openNewTabAndApplicationPanel(url) {
+  let tab = await addTab(url);
+  let target = TargetFactory.forTab(tab);
+  await target.makeRemote();
+
+  let toolbox = await gDevTools.showToolbox(target, "application");
+  let panel = toolbox.getCurrentPanel();
+  return { panel, tab, target, toolbox };
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/service-workers/dynamic-registration.html
@@ -0,0 +1,19 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+window.registerServiceWorker = function() {
+  window.sw = navigator.serviceWorker.register("empty-sw.js");
+};
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/service-workers/empty-sw.js
@@ -0,0 +1,4 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Empty, just test registering.
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/service-workers/scope-page.html
@@ -0,0 +1,19 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+
+window.sw = navigator.serviceWorker.register("empty-sw.js", {
+  scope: "./scope-page.html"
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/test/service-workers/simple.html
@@ -0,0 +1,15 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Service worker test</title>
+</head>
+<body>
+<script type="text/javascript">
+"use strict";
+window.sw = navigator.serviceWorker.register("empty-sw.js");
+</script>
+</body>
+</html>
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -93,23 +93,26 @@ var connect = async function() {
 
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true);
   Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
   Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
   Services.prefs.setBoolPref("devtools.command-button-noautohide.enabled", true);
-  Services.prefs.setBoolPref("devtools.scratchpad.enabled", true);
   // Bug 1225160 - Using source maps with browser debugging can lead to a crash
   Services.prefs.setBoolPref("devtools.debugger.source-maps-enabled", false);
   Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
   Services.prefs.setBoolPref("devtools.preference.new-panel-enabled", false);
   Services.prefs.setBoolPref("layout.css.emulate-moz-box-with-flex", false);
+
+  Services.prefs.setBoolPref("devtools.accessibility.enabled", true);
+  Services.prefs.setBoolPref("devtools.performance.enabled", false);
 }
+
 window.addEventListener("load", async function() {
   let cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.addEventListener("command", onCloseCommand);
   setPrefDefaults();
   // Reveal status message if connecting is slow or if an error occurs.
   let delayedStatusReveal = setTimeout(revealStatusMessage, STATUS_REVEAL_TIME);
   try {
     await connect();
--- a/devtools/client/webconsole/test/fixtures/stubs/pageError.js
+++ b/devtools/client/webconsole/test/fixtures/stubs/pageError.js
@@ -39,24 +39,24 @@ stubPreparedMessages.set(`ReferenceError
     },
     {
       "filename": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html",
       "lineNumber": 9,
       "columnNumber": 3,
       "functionName": null
     },
     {
-      "filename": "resource://testing-common/content-task.js line 50 > eval",
+      "filename": "resource://testing-common/content-task.js line 55 > eval",
       "lineNumber": 7,
       "columnNumber": 9,
       "functionName": null
     },
     {
       "filename": "resource://testing-common/content-task.js",
-      "lineNumber": 51,
+      "lineNumber": 56,
       "columnNumber": 20,
       "functionName": null
     }
   ],
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html",
     "line": 3,
     "column": 5
@@ -78,24 +78,24 @@ stubPreparedMessages.set(`SyntaxError: r
   "type": "log",
   "helperType": null,
   "level": "error",
   "messageText": "SyntaxError: redeclaration of let a",
   "parameters": null,
   "repeatId": "{\"frame\":{\"source\":\"http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html\",\"line\":2,\"column\":9},\"groupId\":null,\"indent\":0,\"level\":\"error\",\"messageText\":\"SyntaxError: redeclaration of let a\",\"parameters\":null,\"source\":\"javascript\",\"type\":\"log\",\"userProvidedStyles\":null,\"private\":false}",
   "stacktrace": [
     {
-      "filename": "resource://testing-common/content-task.js line 50 > eval",
+      "filename": "resource://testing-common/content-task.js line 55 > eval",
       "lineNumber": 7,
       "columnNumber": 9,
       "functionName": null
     },
     {
       "filename": "resource://testing-common/content-task.js",
-      "lineNumber": 51,
+      "lineNumber": 56,
       "columnNumber": 20,
       "functionName": null
     }
   ],
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html",
     "line": 2,
     "column": 9
@@ -136,24 +136,24 @@ stubPreparedMessages.set(`TypeError long
   "stacktrace": [
     {
       "filename": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html",
       "lineNumber": 1,
       "columnNumber": 7,
       "functionName": null
     },
     {
-      "filename": "resource://testing-common/content-task.js line 50 > eval",
+      "filename": "resource://testing-common/content-task.js line 55 > eval",
       "lineNumber": 7,
       "columnNumber": 9,
       "functionName": null
     },
     {
       "filename": "resource://testing-common/content-task.js",
-      "lineNumber": 51,
+      "lineNumber": 56,
       "columnNumber": 20,
       "functionName": null
     }
   ],
   "frame": {
     "source": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html",
     "line": 1,
     "column": 7
@@ -242,24 +242,24 @@ stubPackets.set(`ReferenceError: asdf is
       },
       {
         "filename": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html",
         "lineNumber": 9,
         "columnNumber": 3,
         "functionName": null
       },
       {
-        "filename": "resource://testing-common/content-task.js line 50 > eval",
+        "filename": "resource://testing-common/content-task.js line 55 > eval",
         "lineNumber": 7,
         "columnNumber": 9,
         "functionName": null
       },
       {
         "filename": "resource://testing-common/content-task.js",
-        "lineNumber": 51,
+        "lineNumber": 56,
         "columnNumber": 20,
         "functionName": null
       }
     ],
     "notes": null
   }
 });
 
@@ -278,24 +278,24 @@ stubPackets.set(`SyntaxError: redeclarat
     "warning": false,
     "error": false,
     "exception": true,
     "strict": false,
     "info": false,
     "private": false,
     "stacktrace": [
       {
-        "filename": "resource://testing-common/content-task.js line 50 > eval",
+        "filename": "resource://testing-common/content-task.js line 55 > eval",
         "lineNumber": 7,
         "columnNumber": 9,
         "functionName": null
       },
       {
         "filename": "resource://testing-common/content-task.js",
-        "lineNumber": 51,
+        "lineNumber": 56,
         "columnNumber": 20,
         "functionName": null
       }
     ],
     "notes": [
       {
         "messageBody": "Previously declared at line 2, column 6",
         "frame": {
@@ -334,24 +334,24 @@ stubPackets.set(`TypeError longString me
     "stacktrace": [
       {
         "filename": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/test-console-api.html",
         "lineNumber": 1,
         "columnNumber": 7,
         "functionName": null
       },
       {
-        "filename": "resource://testing-common/content-task.js line 50 > eval",
+        "filename": "resource://testing-common/content-task.js line 55 > eval",
         "lineNumber": 7,
         "columnNumber": 9,
         "functionName": null
       },
       {
         "filename": "resource://testing-common/content-task.js",
-        "lineNumber": 51,
+        "lineNumber": 56,
         "columnNumber": 20,
         "functionName": null
       }
     ],
     "notes": null
   }
 });
 
--- a/devtools/shared/client/root-client.js
+++ b/devtools/shared/client/root-client.js
@@ -101,26 +101,20 @@ RootClient.prototype = {
    *         - {Array} service
    *           array of form-like objects for serviceworkers
    *         - {Array} shared
    *           Array of WorkerActor forms, containing shared workers.
    *         - {Array} other
    *           Array of WorkerActor forms, containing other workers.
    */
   listAllWorkers: async function() {
-    let result = {
-      service: [],
-      shared: [],
-      other: []
-    };
+    let registrations = [];
+    let workers = [];
 
     try {
-      let registrations = [];
-      let workers = [];
-
       // List service worker registrations
       ({ registrations } = await this.listServiceWorkerRegistrations());
 
       // List workers from the Parent process
       ({ workers } = await this.listWorkers());
 
       // And then from the Child processes
       let { processes } = await this.listProcesses();
@@ -132,67 +126,73 @@ RootClient.prototype = {
         let { form } = await this._client.getProcess(process.id);
         let processActor = form.actor;
         let response = await this._client.request({
           to: processActor,
           type: "listWorkers"
         });
         workers = workers.concat(response.workers);
       }
-
-      registrations.forEach(form => {
-        result.service.push({
-          name: form.url,
-          url: form.url,
-          scope: form.scope,
-          fetch: form.fetch,
-          registrationActor: form.actor,
-          active: form.active
-        });
-      });
-
-      workers.forEach(form => {
-        let worker = {
-          name: form.url,
-          url: form.url,
-          workerActor: form.actor
-        };
-        switch (form.type) {
-          case Ci.nsIWorkerDebugger.TYPE_SERVICE:
-            let registration = result.service.find(r => r.scope === form.scope);
-            if (registration) {
-              // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
-              // have a scriptSpec, but its associated WorkerDebugger does.
-              if (!registration.url) {
-                registration.name = registration.url = form.url;
-              }
-              registration.workerActor = form.actor;
-            } else {
-              worker.fetch = form.fetch;
-
-              // If a service worker registration could not be found, this means we are in
-              // e10s, and registrations are not forwarded to other processes until they
-              // reach the activated state. Augment the worker as a registration worker to
-              // display it in aboutdebugging.
-              worker.scope = form.scope;
-              worker.active = false;
-              result.service.push(worker);
-            }
-            break;
-          case Ci.nsIWorkerDebugger.TYPE_SHARED:
-            result.shared.push(worker);
-            break;
-          default:
-            result.other.push(worker);
-        }
-      });
     } catch (e) {
       // Something went wrong, maybe our client is disconnected?
     }
 
+    let result = {
+      service: [],
+      shared: [],
+      other: []
+    };
+
+    registrations.forEach(form => {
+      result.service.push({
+        name: form.url,
+        url: form.url,
+        scope: form.scope,
+        fetch: form.fetch,
+        registrationActor: form.actor,
+        active: form.active
+      });
+    });
+
+    workers.forEach(form => {
+      let worker = {
+        name: form.url,
+        url: form.url,
+        workerActor: form.actor
+      };
+      switch (form.type) {
+        case Ci.nsIWorkerDebugger.TYPE_SERVICE:
+          let registration = result.service.find(r => r.scope === form.scope);
+          if (registration) {
+            // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't
+            // have a scriptSpec, but its associated WorkerDebugger does.
+            if (!registration.url) {
+              registration.name = registration.url = form.url;
+            }
+            registration.workerActor = form.actor;
+          } else {
+            worker.fetch = form.fetch;
+
+            // If a service worker registration could not be found, this means we are in
+            // e10s, and registrations are not forwarded to other processes until they
+            // reach the activated state. Augment the worker as a registration worker to
+            // display it in aboutdebugging.
+            worker.scope = form.scope;
+            worker.active = false;
+            result.service.push(worker);
+          }
+          break;
+        case Ci.nsIWorkerDebugger.TYPE_SHARED:
+          result.shared.push(worker);
+          break;
+        default:
+          result.other.push(worker);
+      }
+    });
+
     return result;
   },
 
   /**
    * Fetch the TabActor for the currently selected tab, or for a specific
    * tab given as first parameter.
    *
    * @param [optional] object filter
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/shared/gcli/commands/screenshot.js
@@ -1,15 +1,16 @@
 /* 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";
 
 const { Cc, Ci, Cr, Cu } = require("chrome");
+const ChromeUtils = require("ChromeUtils");
 const l10n = require("gcli/l10n");
 const Services = require("Services");
 const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
 const { getRect } = require("devtools/shared/layout/utils");
 const defer = require("devtools/shared/defer");
 const { Task } = require("devtools/shared/task");
 
 loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
--- a/dom/browser-element/BrowserElementChildPreload.js
+++ b/dom/browser-element/BrowserElementChildPreload.js
@@ -872,17 +872,17 @@ BrowserElementChild.prototype = {
     if (ChromeUtils.getClassName(elem) === "HTMLVideoElement" ||
         ChromeUtils.getClassName(elem) === "HTMLAudioElement") {
       let hasVideo = !(elem.readyState >= elem.HAVE_METADATA &&
                        (elem.videoWidth == 0 || elem.videoHeight == 0));
       return {uri: elem.currentSrc || elem.src,
               hasVideo: hasVideo,
               documentURI: documentURI};
     }
-    if (elem instanceof Ci.nsIDOMHTMLInputElement &&
+    if (ChromeUtils.getClassName(elem) === "HTMLInputElement" &&
         elem.hasAttribute("name")) {
       // For input elements, we look for a parent <form> and if there is
       // one we return the form's method and action uri.
       let parent = elem.parentNode;
       while (parent) {
         if (ChromeUtils.getClassName(parent) === "HTMLFormElement" &&
             parent.hasAttribute("action")) {
           let actionHref = docShell.QueryInterface(Ci.nsIWebNavigation)
--- a/dom/canvas/CanvasRenderingContext2D.cpp
+++ b/dom/canvas/CanvasRenderingContext2D.cpp
@@ -1080,17 +1080,20 @@ static int32_t sMaxContexts = 0;
 
 
 
 CanvasRenderingContext2D::CanvasRenderingContext2D(layers::LayersBackend aCompositorBackend)
   : mRenderingMode(RenderingMode::OpenGLBackendMode)
   , mCompositorBackend(aCompositorBackend)
   // these are the default values from the Canvas spec
   , mWidth(0), mHeight(0)
-  , mZero(false), mOpaque(false)
+  , mZero(false)
+  , mOpaqueAttrValue(false)
+  , mContextAttributesHasAlpha(true)
+  , mOpaque(false)
   , mResetLayer(true)
   , mIPC(false)
   , mIsSkiaGL(false)
   , mHasPendingStableStateCallback(false)
   , mDrawObserver(nullptr)
   , mIsEntireFrameInvalid(false)
   , mPredictManyRedrawCalls(false)
   , mIsCapturedFrameInvalid(false)
@@ -1990,22 +1993,29 @@ CanvasRenderingContext2D::InitializeWith
     // Cf comment in EnsureTarget
     mTarget->PushClipRect(gfx::Rect(Point(0, 0), Size(mWidth, mHeight)));
   }
 
   return NS_OK;
 }
 
 void
-CanvasRenderingContext2D::SetIsOpaque(bool aIsOpaque)
-{
-  if (aIsOpaque != mOpaque) {
-    mOpaque = aIsOpaque;
-    ClearTarget();
-  }
+CanvasRenderingContext2D::SetOpaqueValueFromOpaqueAttr(bool aOpaqueAttrValue)
+{
+  if (aOpaqueAttrValue != mOpaqueAttrValue) {
+    mOpaqueAttrValue = aOpaqueAttrValue;
+    UpdateIsOpaque();
+  }
+}
+
+void
+CanvasRenderingContext2D::UpdateIsOpaque()
+{
+  mOpaque = !mContextAttributesHasAlpha || mOpaqueAttrValue;
+  ClearTarget();
 }
 
 NS_IMETHODIMP
 CanvasRenderingContext2D::SetIsIPC(bool aIsIPC)
 {
   if (aIsIPC != mIPC) {
     mIPC = aIsIPC;
     ClearTarget();
@@ -2038,19 +2048,18 @@ CanvasRenderingContext2D::SetContextOpti
 
       // We want to lock into software, so remove the observer that
       // may potentially change that...
       RemoveDrawObserver();
       mRenderingMode = RenderingMode::SoftwareBackendMode;
     }
   }
 
-  if (!attributes.mAlpha) {
-    SetIsOpaque(true);
-  }
+  mContextAttributesHasAlpha = attributes.mAlpha;
+  UpdateIsOpaque();
 
   return NS_OK;
 }
 
 UniquePtr<uint8_t[]>
 CanvasRenderingContext2D::GetImageBuffer(int32_t* aFormat)
 {
   UniquePtr<uint8_t[]> ret;
--- a/dom/canvas/CanvasRenderingContext2D.h
+++ b/dom/canvas/CanvasRenderingContext2D.h
@@ -455,17 +455,17 @@ public:
   {
     EnsureTarget();
     if (aOutAlphaType) {
       *aOutAlphaType = (mOpaque ? gfxAlphaType::Opaque : gfxAlphaType::Premult);
     }
     return mTarget->Snapshot();
   }
 
-  virtual void SetIsOpaque(bool aIsOpaque) override;
+  virtual void SetOpaqueValueFromOpaqueAttr(bool aOpaqueAttrValue) override;
   bool GetIsOpaque() override { return mOpaque; }
   NS_IMETHOD Reset() override;
   already_AddRefed<Layer> GetCanvasLayer(nsDisplayListBuilder* aBuilder,
                                          Layer* aOldLayer,
                                          LayerManager* aManager) override;
 
   bool UpdateWebRenderCanvasData(nsDisplayListBuilder* aBuilder,
                                  WebRenderCanvasData* aCanvasData) override;
@@ -614,16 +614,19 @@ protected:
    // Returns whether a filter was successfully parsed.
   bool ParseFilter(const nsAString& aString,
                    nsTArray<nsStyleFilter>& aFilterChain,
                    ErrorResult& aError);
 
   // Returns whether the font was successfully updated.
   bool SetFontInternal(const nsAString& aFont, mozilla::ErrorResult& aError);
 
+  // Clears the target and updates mOpaque based on mOpaqueAttrValue and
+  // mContextAttributesHasAlpha.
+  void UpdateIsOpaque();
 
   /**
    * Creates the error target, if it doesn't exist
    */
   static void EnsureErrorTarget();
 
   /* This function ensures there is a writable pathbuilder available, this
    * pathbuilder may be working in user space or in device space or
@@ -759,16 +762,27 @@ protected:
 
   // Member vars
   int32_t mWidth, mHeight;
 
   // This is true when the canvas is valid, but of zero size, this requires
   // specific behavior on some operations.
   bool mZero;
 
+  // The two ways to set the opaqueness of the canvas.
+  // mOpaqueAttrValue: Whether the <canvas> element has the moz-opaque attribute
+  // set. Can change during the lifetime of the context. Non-standard, should
+  // hopefully go away soon.
+  // mContextAttributesHasAlpha: The standard way of setting canvas opaqueness.
+  // Set at context initialization time and never changes.
+  bool mOpaqueAttrValue;
+  bool mContextAttributesHasAlpha;
+
+  // Determines the context's opaqueness. Is computed from mOpaqueAttrValue and
+  // mContextAttributesHasAlpha in UpdateIsOpaque().
   bool mOpaque;
 
   // This is true when the next time our layer is retrieved we need to
   // recreate it (i.e. our backing surface changed)
   bool mResetLayer;
   // This is needed for drawing in drawAsyncXULElement
   bool mIPC;
   // True if the current DrawTarget is using skia-gl, used so we can avoid
--- a/dom/canvas/CanvasRenderingContextHelper.cpp
+++ b/dom/canvas/CanvasRenderingContextHelper.cpp
@@ -226,17 +226,17 @@ CanvasRenderingContextHelper::UpdateCont
 {
   if (!mCurrentContext)
     return NS_OK;
 
   nsIntSize sz = GetWidthHeight();
 
   nsCOMPtr<nsICanvasRenderingContextInternal> currentContext = mCurrentContext;
 
-  currentContext->SetIsOpaque(GetOpaqueAttr());
+  currentContext->SetOpaqueValueFromOpaqueAttr(GetOpaqueAttr());
 
   nsresult rv = currentContext->SetContextOptions(aCx, aNewContextOptions,
                                          aRvForDictionaryInit);
   if (NS_FAILED(rv)) {
     mCurrentContext = nullptr;
     return rv;
   }
 
--- a/dom/canvas/ImageBitmapRenderingContext.cpp
+++ b/dom/canvas/ImageBitmapRenderingContext.cpp
@@ -184,18 +184,19 @@ ImageBitmapRenderingContext::GetSurfaceS
   if (surface->GetSize() != IntSize(mWidth, mHeight)) {
     return MatchWithIntrinsicSize();
   }
 
   return surface.forget();
 }
 
 void
-ImageBitmapRenderingContext::SetIsOpaque(bool aIsOpaque)
+ImageBitmapRenderingContext::SetOpaqueValueFromOpaqueAttr(bool aOpaqueAttrValue)
 {
+  // ignored
 }
 
 bool
 ImageBitmapRenderingContext::GetIsOpaque()
 {
   return false;
 }
 
--- a/dom/canvas/ImageBitmapRenderingContext.h
+++ b/dom/canvas/ImageBitmapRenderingContext.h
@@ -61,17 +61,17 @@ public:
   virtual mozilla::UniquePtr<uint8_t[]> GetImageBuffer(int32_t* aFormat) override;
   NS_IMETHOD GetInputStream(const char* aMimeType,
                             const char16_t* aEncoderOptions,
                             nsIInputStream** aStream) override;
 
   virtual already_AddRefed<mozilla::gfx::SourceSurface>
   GetSurfaceSnapshot(gfxAlphaType* aOutAlphaType) override;
 
-  virtual void SetIsOpaque(bool aIsOpaque) override;
+  virtual void SetOpaqueValueFromOpaqueAttr(bool aOpaqueAttrValue) override;
   virtual bool GetIsOpaque() override;
   NS_IMETHOD Reset() override;
   virtual already_AddRefed<Layer> GetCanvasLayer(nsDisplayListBuilder* aBuilder,
                                                  Layer* aOldLayer,
                                                  LayerManager* aManager) override;
   virtual void MarkContextClean() override;
 
   NS_IMETHOD Redraw(const gfxRect& aDirty) override;
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -375,17 +375,17 @@ public:
     virtual UniquePtr<uint8_t[]> GetImageBuffer(int32_t* out_format) override;
     NS_IMETHOD GetInputStream(const char* mimeType,
                               const char16_t* encoderOptions,
                               nsIInputStream** out_stream) override;
 
     virtual already_AddRefed<mozilla::gfx::SourceSurface>
     GetSurfaceSnapshot(gfxAlphaType* out_alphaType) override;
 
-    virtual void SetIsOpaque(bool) override {};
+    virtual void SetOpaqueValueFromOpaqueAttr(bool) override {};
     bool GetIsOpaque() override { return !mOptions.alpha; }
     NS_IMETHOD SetContextOptions(JSContext* cx,
                                  JS::Handle<JS::Value> options,
                                  ErrorResult& aRvForDictionaryInit) override;
 
     NS_IMETHOD SetIsIPC(bool) override {
         return NS_ERROR_NOT_IMPLEMENTED;
     }
--- a/dom/canvas/nsICanvasRenderingContextInternal.h
+++ b/dom/canvas/nsICanvasRenderingContextInternal.h
@@ -121,21 +121,26 @@ public:
   // This gets an Azure SourceSurface for the canvas, this will be a snapshot
   // of the canvas at the time it was called.
   // If premultAlpha is provided, then it assumed the callee can handle
   // un-premultiplied surfaces, and *premultAlpha will be set to false
   // if one is returned.
   virtual already_AddRefed<mozilla::gfx::SourceSurface>
   GetSurfaceSnapshot(gfxAlphaType* out_alphaType = nullptr) = 0;
 
-  // If this context is opaque, the backing store of the canvas should
+  // If this is called with true, the backing store of the canvas should
   // be created as opaque; all compositing operators should assume the
-  // dst alpha is always 1.0.  If this is never called, the context
-  // defaults to false (not opaque).
-  virtual void SetIsOpaque(bool isOpaque) = 0;
+  // dst alpha is always 1.0.  If this is never called, the context's
+  // opaqueness is determined by the context attributes that it's initialized
+  // with.
+  virtual void SetOpaqueValueFromOpaqueAttr(bool aOpaqueAttrValue) = 0;
+
+  // Returns whether the context is opaque. This value can be based both on
+  // the value of the moz-opaque attribute and on the context's initialization
+  // attributes.
   virtual bool GetIsOpaque() = 0;
 
   // Invalidate this context and release any held resources, in preperation
   // for possibly reinitializing with SetDimensions/InitializeWithSurface.
   NS_IMETHOD Reset() = 0;
 
   // Return the CanvasLayer for this context, creating
   // one for the given layer manager if not available.
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -1109,17 +1109,16 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
   if (tmp->mFileData) {
     tmp->mFileData->Unlink();
   }
   //XXX should unlink more?
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLInputElement,
                                              nsGenericHTMLFormElementWithState,
-                                             nsIDOMHTMLInputElement,
                                              nsITextControlElement,
                                              imgINotificationObserver,
                                              nsIImageLoadingContent,
                                              nsIDOMNSEditableElement,
                                              nsIConstraintValidation)
 
 // nsIDOMNode
 
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -5,17 +5,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_HTMLInputElement_h
 #define mozilla_dom_HTMLInputElement_h
 
 #include "mozilla/Attributes.h"
 #include "nsGenericHTMLElement.h"
 #include "nsImageLoadingContent.h"
-#include "nsIDOMHTMLInputElement.h"
 #include "nsITextControlElement.h"
 #include "nsITimer.h"
 #include "nsIDOMNSEditableElement.h"
 #include "nsCOMPtr.h"
 #include "nsIConstraintValidation.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/HTMLFormElement.h" // for HasEverTriedInvalidSubmit()
@@ -120,17 +119,16 @@ public:
     nsCOMPtr<nsIFilePicker> mFilePicker;
     nsCOMPtr<nsIFilePickerShownCallback> mFpCallback;
     nsCOMPtr<nsIContentPref> mResult;
   };
 };
 
 class HTMLInputElement final : public nsGenericHTMLFormElementWithState,
                                public nsImageLoadingContent,
-                               public nsIDOMHTMLInputElement,
                                public nsITextControlElement,
                                public nsIDOMNSEditableElement,
                                public nsIConstraintValidation
 {
   friend class AfterSetFilesOrDirectoriesCallback;
   friend class DispatchChangeEventCallback;
   friend class ::InputType;
 
@@ -161,19 +159,16 @@ public:
 #endif
 
   // Element
   virtual bool IsInteractiveHTMLContent(bool aIgnoreTabindex) const override;
 
   // EventTarget
   virtual void AsyncEventRunning(AsyncEventDispatcher* aEvent) override;
 
-  // nsIDOMHTMLInputElement
-  NS_DECL_NSIDOMHTMLINPUTELEMENT
-
   // nsIDOMNSEditableElement
   NS_IMETHOD GetEditor(nsIEditor** aEditor) override
   {
     nsCOMPtr<nsIEditor> editor = GetEditor();
     editor.forget(aEditor);
     return NS_OK;
   }
 
--- a/dom/interfaces/html/moz.build
+++ b/dom/interfaces/html/moz.build
@@ -3,15 +3,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/.
 
 with Files("**"):
     BUG_COMPONENT = ("Core", "DOM")
 
 XPIDL_SOURCES += [
-    'nsIDOMHTMLInputElement.idl',
     'nsIDOMMozBrowserFrame.idl',
     'nsIMozBrowserFrame.idl',
 ]
 
 XPIDL_MODULE = 'dom_html'
 
deleted file mode 100644
--- a/dom/interfaces/html/nsIDOMHTMLInputElement.idl
+++ /dev/null
@@ -1,22 +0,0 @@
-/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "nsISupports.idl"
-
- /**
-  * The nsIDOMHTMLInputElement interface is the interface to a [X]HTML
-  * input element.
-  *
-  * This interface is trying to follow the DOM Level 2 HTML specification:
-  * http://www.w3.org/TR/DOM-Level-2-HTML/
-  *
-  * with changes from the work-in-progress WHATWG HTML specification:
-  * http://www.whatwg.org/specs/web-apps/current-work/
-  */
-
-[shim(HTMLInputElement), uuid(64aeda0b-e9b5-4868-a4f9-e4776e32e733)]
-interface nsIDOMHTMLInputElement : nsISupports
-{
-};
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -1979,20 +1979,21 @@ APZCTreeManager::SetTargetAPZC(uint64_t 
 }
 
 void
 APZCTreeManager::UpdateZoomConstraints(const ScrollableLayerGuid& aGuid,
                                        const Maybe<ZoomConstraints>& aConstraints)
 {
   if (!GetUpdater()->IsUpdaterThread()) {
     // This can happen if we're in the UI process and got a call directly from
-    // nsBaseWidget (as opposed to over PAPZCTreeManager). We want this function
-    // to run on the updater thread, so bounce it over.
-    MOZ_ASSERT(XRE_IsParentProcess());
-
+    // nsBaseWidget or from a content process over PAPZCTreeManager. In that case
+    // we get this call on the compositor thread, which may be different from
+    // the updater thread. It can also happen in the GPU process if that is
+    // enabled, since the call will go over PAPZCTreeManager and arrive on the
+    // compositor thread in the GPU process.
     GetUpdater()->RunOnUpdaterThread(
         aGuid.mLayersId,
         NewRunnableMethod<ScrollableLayerGuid, Maybe<ZoomConstraints>>(
             "APZCTreeManager::UpdateZoomConstraints",
             this,
             &APZCTreeManager::UpdateZoomConstraints,
             aGuid,
             aConstraints));
--- a/gfx/layers/wr/WebRenderLayerManager.cpp
+++ b/gfx/layers/wr/WebRenderLayerManager.cpp
@@ -259,31 +259,32 @@ WebRenderLayerManager::EndTransactionWit
 #ifdef XP_WIN
   gfxDWriteFont::UpdateClearTypeUsage();
 #endif
 
   // Since we don't do repeat transactions right now, just set the time
   mAnimationReadyTime = TimeStamp::Now();
 
   WrBridge()->BeginTransaction();
-  DiscardCompositorAnimations();
 
   LayoutDeviceIntSize size = mWidget->GetClientSize();
   wr::LayoutSize contentSize { (float)size.width, (float)size.height };
   wr::DisplayListBuilder builder(WrBridge()->GetPipeline(), contentSize, mLastDisplayListSize);
   wr::IpcResourceUpdateQueue resourceUpdates(WrBridge());
 
   mWebRenderCommandBuilder.BuildWebRenderCommands(builder,
                                                   resourceUpdates,
                                                   aDisplayList,
                                                   aDisplayListBuilder,
                                                   mScrollData,
                                                   contentSize,
                                                   aFilters);
 
+  DiscardCompositorAnimations();
+
   mWidget->AddWindowOverlayWebRenderCommands(WrBridge(), builder, resourceUpdates);
   mWindowOverlayChanged = false;
 
   if (AsyncPanZoomEnabled()) {
     mScrollData.SetFocusTarget(mFocusTarget);
     mFocusTarget = FocusTarget();
 
     if (mIsFirstPaint) {
--- a/gfx/layers/wr/WebRenderScrollData.cpp
+++ b/gfx/layers/wr/WebRenderScrollData.cpp
@@ -37,45 +37,58 @@ WebRenderLayerScrollData::InitializeRoot
   mDescendantCount = aDescendantCount;
 }
 
 void
 WebRenderLayerScrollData::Initialize(WebRenderScrollData& aOwner,
                                      nsDisplayItem* aItem,
                                      int32_t aDescendantCount,
                                      const ActiveScrolledRoot* aStopAtAsr,
-                                     const Maybe<gfx::Matrix4x4>& aTransform)
+                                     const Maybe<gfx::Matrix4x4>& aAncestorTransform)
 {
   MOZ_ASSERT(aDescendantCount >= 0); // Ensure value is valid
   MOZ_ASSERT(mDescendantCount == -1); // Don't allow re-setting an already set value
   mDescendantCount = aDescendantCount;
 
   MOZ_ASSERT(aItem);
   aItem->UpdateScrollData(&aOwner, this);
-  if (aTransform) {
-    // mTransform might have been set by the UpdateScrollData call above, so
-    // we should combine rather than clobber
-    mTransform = *aTransform * mTransform;
-  }
   for (const ActiveScrolledRoot* asr = aItem->GetActiveScrolledRoot();
        asr && asr != aStopAtAsr;
        asr = asr->mParent) {
     MOZ_ASSERT(aOwner.GetManager());
     FrameMetrics::ViewID scrollId = asr->GetViewId();
     if (Maybe<size_t> index = aOwner.HasMetadataFor(scrollId)) {
       mScrollIds.AppendElement(index.ref());
     } else {
       Maybe<ScrollMetadata> metadata = asr->mScrollableFrame->ComputeScrollMetadata(
           aOwner.GetManager(), aItem->ReferenceFrame(),
           ContainerLayerParameters(), nullptr);
       MOZ_ASSERT(metadata);
       MOZ_ASSERT(metadata->GetMetrics().GetScrollId() == scrollId);
       mScrollIds.AppendElement(aOwner.AddMetadata(metadata.ref()));
     }
   }
+
+  // aAncestorTransform, if present, is the transform from an ancestor
+  // nsDisplayTransform that was stored on the stacking context in order to
+  // propagate it downwards in the tree. (i.e. |aItem| is a strict descendant of
+  // the nsDisplayTranform which produced aAncestorTransform). We store this
+  // separately from mTransform because in the case where we have multiple
+  // scroll metadata on this layer item, the mAncestorTransform is associated
+  // with the "topmost" scroll metadata, and the mTransform is associated with
+  // the "bottommost" scroll metadata. The code in
+  // WebRenderScrollDataWrapper::GetTransform() is responsible for combining
+  // these transforms and exposing them appropriately. Also, we don't save the
+  // ancestor transform for thumb layers, because those are a special case in
+  // APZ; we need to keep the ancestor transform for the scrollable content that
+  // the thumb scrolls, but not for the thumb itself, as it will result in
+  // incorrect visual positioning of the thumb.
+  if (aAncestorTransform && mScrollbarData.mScrollbarLayerType != ScrollbarLayerType::Thumb) {
+    mAncestorTransform = *aAncestorTransform;
+  }
 }
 
 int32_t
 WebRenderLayerScrollData::GetDescendantCount() const
 {
   MOZ_ASSERT(mDescendantCount >= 0); // check that it was set
   return mDescendantCount;
 }
@@ -109,26 +122,27 @@ WebRenderLayerScrollData::GetTransformTy
 
 void
 WebRenderLayerScrollData::Dump(const WebRenderScrollData& aOwner) const
 {
   printf_stderr("LayerScrollData(%p) descendants %d\n", this, mDescendantCount);
   for (size_t i : mScrollIds) {
     printf_stderr("  metadata: %s\n", Stringify(aOwner.GetScrollMetadata(i)).c_str());
   }
+  printf_stderr("  ancestor transform: %s\n", Stringify(mAncestorTransform).c_str());
   printf_stderr("  transform: %s perspective: %d visible: %s\n",
     Stringify(mTransform).c_str(), mTransformIsPerspective,
     Stringify(mVisibleRegion).c_str());
   printf_stderr("  event regions: %s override: 0x%x\n",
     Stringify(mEventRegions).c_str(), mEventRegionsOverride);
   if (mReferentId) {
     printf_stderr("  ref layers id: 0x%" PRIx64 "\n", uint64_t(*mReferentId));
   }
-  //printf_stderr("  scroll thumb: %s animation: %" PRIu64 "\n",
-  //  Stringify(mScrollThumbData).c_str(), mScrollbarAnimationId);
+  printf_stderr("  scrollbar type: %d animation: %" PRIx64 "\n",
+    (int)mScrollbarData.mScrollbarLayerType, mScrollbarAnimationId);
   printf_stderr("  fixed pos container: %" PRIu64 "\n",
     mFixedPosScrollContainerId);
 }
 
 WebRenderScrollData::WebRenderScrollData()
   : mManager(nullptr)
   , mIsFirstPaint(false)
   , mPaintSequenceNumber(0)
--- a/gfx/layers/wr/WebRenderScrollData.h
+++ b/gfx/layers/wr/WebRenderScrollData.h
@@ -43,30 +43,31 @@ public:
   WebRenderLayerScrollData(); // needed for IPC purposes
   ~WebRenderLayerScrollData();
 
   void InitializeRoot(int32_t aDescendantCount);
   void Initialize(WebRenderScrollData& aOwner,
                   nsDisplayItem* aItem,
                   int32_t aDescendantCount,
                   const ActiveScrolledRoot* aStopAtAsr,
-                  const Maybe<gfx::Matrix4x4>& aTransform);
+                  const Maybe<gfx::Matrix4x4>& aAncestorTransform);
 
   int32_t GetDescendantCount() const;
   size_t GetScrollMetadataCount() const;
 
   void AppendScrollMetadata(WebRenderScrollData& aOwner,
                             const ScrollMetadata& aData);
   // Return the ScrollMetadata object that used to be on the original Layer
   // at the given index. Since we deduplicate the ScrollMetadata objects into
   // the array in the owning WebRenderScrollData object, we need to be passed
   // in a reference to that owner as well.
   const ScrollMetadata& GetScrollMetadata(const WebRenderScrollData& aOwner,
                                           size_t aIndex) const;
 
+  gfx::Matrix4x4 GetAncestorTransform() const { return mAncestorTransform; }
   void SetTransform(const gfx::Matrix4x4& aTransform) { mTransform = aTransform; }
   gfx::Matrix4x4 GetTransform() const { return mTransform; }
   CSSTransformMatrix GetTransformTyped() const;
   void SetTransformIsPerspective(bool aTransformIsPerspective) { mTransformIsPerspective = aTransformIsPerspective; }
   bool GetTransformIsPerspective() const { return mTransformIsPerspective; }
 
   void AddEventRegions(const EventRegions& aRegions) { mEventRegions.OrWith(aRegions); }
   EventRegions GetEventRegions() const { return mEventRegions; }
@@ -100,16 +101,17 @@ private:
   // mScrollMetadatas array. This indirection is used to deduplicate the
   // ScrollMetadata objects, since there is usually heavy duplication of them
   // within a layer tree.
   nsTArray<size_t> mScrollIds;
 
   // Various data that we collect from the Layer in Initialize(), serialize
   // over IPC, and use on the parent side in APZ.
 
+  gfx::Matrix4x4 mAncestorTransform;
   gfx::Matrix4x4 mTransform;
   bool mTransformIsPerspective;
   EventRegions mEventRegions;
   LayerIntRegion mVisibleRegion;
   Maybe<LayersId> mReferentId;
   EventRegionsOverride mEventRegionsOverride;
   ScrollbarData mScrollbarData;
   uint64_t mScrollbarAnimationId;
@@ -211,32 +213,34 @@ struct ParamTraits<mozilla::layers::WebR
 {
   typedef mozilla::layers::WebRenderLayerScrollData paramType;
 
   static void
   Write(Message* aMsg, const paramType& aParam)
   {
     WriteParam(aMsg, aParam.mDescendantCount);
     WriteParam(aMsg, aParam.mScrollIds);
+    WriteParam(aMsg, aParam.mAncestorTransform);
     WriteParam(aMsg, aParam.mTransform);
     WriteParam(aMsg, aParam.mTransformIsPerspective);
     WriteParam(aMsg, aParam.mEventRegions);
     WriteParam(aMsg, aParam.mVisibleRegion);
     WriteParam(aMsg, aParam.mReferentId);
     WriteParam(aMsg, aParam.mEventRegionsOverride);
     WriteParam(aMsg, aParam.mScrollbarData);
     WriteParam(aMsg, aParam.mScrollbarAnimationId);
     WriteParam(aMsg, aParam.mFixedPosScrollContainerId);
   }
 
   static bool
   Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
   {
     return ReadParam(aMsg, aIter, &aResult->mDescendantCount)
         && ReadParam(aMsg, aIter, &aResult->mScrollIds)
+        && ReadParam(aMsg, aIter, &aResult->mAncestorTransform)
         && ReadParam(aMsg, aIter, &aResult->mTransform)
         && ReadParam(aMsg, aIter, &aResult->mTransformIsPerspective)
         && ReadParam(aMsg, aIter, &aResult->mEventRegions)
         && ReadParam(aMsg, aIter, &aResult->mVisibleRegion)
         && ReadParam(aMsg, aIter, &aResult->mReferentId)
         && ReadParam(aMsg, aIter, &aResult->mEventRegionsOverride)
         && ReadParam(aMsg, aIter, &aResult->mScrollbarData)
         && ReadParam(aMsg, aIter, &aResult->mScrollbarAnimationId)
--- a/gfx/layers/wr/WebRenderScrollDataWrapper.h
+++ b/gfx/layers/wr/WebRenderScrollDataWrapper.h
@@ -217,20 +217,30 @@ public:
   {
     return "WebRenderScrollDataWrapper";
   }
 
   gfx::Matrix4x4 GetTransform() const
   {
     MOZ_ASSERT(IsValid());
 
+    // See WebRenderLayerScrollData::Initialize for more context. The ancestor
+    // transform is associated with the "topmost" layer, and the transform is
+    // associated with the "bottommost" layer. If there is only one
+    // scrollmetadata on the layer, then it is both "topmost" and "bottommost"
+    // and we combine the two transforms.
+
+    gfx::Matrix4x4 transform;
+    if (AtTopLayer()) {
+      transform = mLayer->GetAncestorTransform();
+    }
     if (AtBottomLayer()) {
-      return mLayer->GetTransform();
+      transform = transform * mLayer->GetTransform();
     }
-    return gfx::Matrix4x4();
+    return transform;
   }
 
   CSSTransformMatrix GetTransformTyped() const
   {
     return ViewAs<CSSTransformMatrix>(GetTransform());
   }
 
   bool TransformIsPerspective() const
new file mode 100644
--- /dev/null
+++ b/gfx/tests/reftest/1451168-ref.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <style type="text/css">
+html {
+    height:100%;
+    margin:0;
+    padding: 0;
+}
+body {
+    height:100%;
+    margin: 0;
+    padding: 0;
+    overflow:hidden;
+    position:relative;
+    left:0;
+}
+nav {
+    display:block;
+    position:absolute;
+    top:0;
+    left:0;
+    bottom:auto;
+    width:280px;
+    height:100%;
+    z-index:1000;
+}
+
+.nav-menu-inner-container {
+    position:relative;
+    height:100%;
+}
+
+.nav-menu-scroll-pane {
+    bottom:100px;
+    overflow-y:scroll;
+    position:absolute;
+    top:0;
+    width:254px
+}
+  </style>
+</head>
+
+<body>
+<nav class="moz-global-nav-drawer">
+  <div class="nav-menu-inner-container">
+    <div class="nav-menu-scroll-pane">
+      <div style="height: 5000px">
+        Scroll here and look for the scrollbar
+      </div>
+    </div>
+  </div>
+</nav>
+</body></html>
new file mode 100644
--- /dev/null
+++ b/gfx/tests/reftest/1451168.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <style type="text/css">
+html {
+    height:100%;
+    margin:0;
+    padding: 0;
+}
+body {
+    height:100%;
+    margin: 0;
+    padding: 0;
+    overflow:hidden;
+    position:relative;
+    left:0;
+    transform:translateX(320px);
+}
+nav {
+    display:block;
+    position:absolute;
+    top:0;
+    left:0;
+    bottom:auto;
+    width:280px;
+    height:100%;
+    z-index:1000;
+    transform:translateX(-320px);
+}
+
+.nav-menu-inner-container {
+    position:relative;
+    height:100%;
+}
+
+.nav-menu-scroll-pane {
+    bottom:100px;
+    overflow-y:scroll;
+    position:absolute;
+    top:0;
+    width:254px
+}
+  </style>
+</head>
+
+<body>
+<nav class="moz-global-nav-drawer">
+  <div class="nav-menu-inner-container">
+    <div class="nav-menu-scroll-pane">
+      <div style="height: 5000px">
+        Scroll here and look for the scrollbar
+      </div>
+    </div>
+  </div>
+</nav>
+</body></html>
--- a/gfx/tests/reftest/reftest.list
+++ b/gfx/tests/reftest/reftest.list
@@ -7,8 +7,9 @@ skip-if(!asyncPan) == 1086723.html 10867
 skip-if(Android) fuzzy-if(skiaContent,1,587) == 1143303-1.svg pass.svg
 fuzzy(100,30) == 1149923.html 1149923-ref.html # use fuzzy due to few distorted pixels caused by border-radius
 == 1131264-1.svg pass.svg
 == 1419528.html 1419528-ref.html
 == 1424673.html 1424673-ref.html
 == 1429411.html 1429411-ref.html
 == 1435143.html 1435143-ref.html
 == 1444904.html 1444904-ref.html
+== 1451168.html 1451168-ref.html
--- a/js/src/frontend/BinSource-auto.cpp
+++ b/js/src/frontend/BinSource-auto.cpp
@@ -4725,18 +4725,18 @@ BinASTParser<Tok>::parseInterfaceEagerFu
     BINJS_TRY_VAR(body, factory_.newLexicalScope(*lexicalScopeData, body));
     BINJS_MOZ_TRY_DECL(result, buildFunction(start, kind, name, params, body, funbox));
     return result;
 }
 
 
 /*
  interface EagerGetter : Node {
+    PropertyName name;
     AssertedVarScope? bodyScope;
-    PropertyName name;
     FunctionBody body;
  }
 */
 template<typename Tok> JS::Result<ParseNode*>
 BinASTParser<Tok>::parseEagerGetter()
 {
     BinKind kind;
     BinFields fields(cx_);
@@ -4754,45 +4754,45 @@ BinASTParser<Tok>::parseEagerGetter()
 template<typename Tok> JS::Result<ParseNode*>
 BinASTParser<Tok>::parseInterfaceEagerGetter(const size_t start, const BinKind kind, const BinFields& fields)
 {
     MOZ_ASSERT(kind == BinKind::EagerGetter);
     CheckRecursionLimit(cx_);
 
 
 #if defined(DEBUG)
-    const BinField expected_fields[3] = { BinField::BodyScope, BinField::Name, BinField::Body };
+    const BinField expected_fields[3] = { BinField::Name, BinField::BodyScope, BinField::Body };
     MOZ_TRY(tokenizer_->checkFields(kind, fields, expected_fields));
 #endif // defined(DEBUG)
 
 
 
 
-    MOZ_TRY(parseOptionalAssertedVarScope());
-
-
-
-
     BINJS_MOZ_TRY_DECL(name, parsePropertyName());
-
-
     BINJS_MOZ_TRY_DECL(funbox, buildFunctionBox(
         GeneratorKind::NotGenerator,
         FunctionAsyncKind::SyncFunction,
-        FunctionSyntaxKind::Getter, name));
+        FunctionSyntaxKind::Getter, /* name = */ nullptr));
 
     // Push a new ParseContext. It will be used to parse `scope`, the arguments, the function.
     BinParseContext funpc(cx_, this, funbox, /* newDirectives = */ nullptr);
     BINJS_TRY(funpc.init());
     parseContext_->functionScope().useAsVarScope(parseContext_);
     MOZ_ASSERT(parseContext_->isFunctionBox());
 
     ParseContext::Scope lexicalScope(cx_, parseContext_, usedNames_);
     BINJS_TRY(lexicalScope.init(parseContext_));
 
+
+
+    MOZ_TRY(parseOptionalAssertedVarScope());
+
+
+
+
     BINJS_MOZ_TRY_DECL(body, parseFunctionBody());
 
 
     ParseNode* params = new_<ListNode>(ParseNodeKind::ParamsBody, tokenizer_->pos(start));
     BINJS_MOZ_TRY_DECL(method, buildFunction(start, kind, name, params, body, funbox));
     BINJS_TRY_DECL(result, factory_.newObjectMethodOrPropertyDefinition(name, method, AccessorType::Getter));
     return result;
 }
@@ -4929,46 +4929,46 @@ BinASTParser<Tok>::parseInterfaceEagerSe
     const BinField expected_fields[5] = { BinField::Name, BinField::ParameterScope, BinField::BodyScope, BinField::Param, BinField::Body };
     MOZ_TRY(tokenizer_->checkFields(kind, fields, expected_fields));
 #endif // defined(DEBUG)
 
 
 
 
     BINJS_MOZ_TRY_DECL(name, parsePropertyName());
-
+    BINJS_MOZ_TRY_DECL(funbox, buildFunctionBox(
+        GeneratorKind::NotGenerator,
+        FunctionAsyncKind::SyncFunction,
+        FunctionSyntaxKind::Setter, /* name = */ nullptr));
+
+    // Push a new ParseContext. It will be used to parse `scope`, the arguments, the function.
+    BinParseContext funpc(cx_, this, funbox, /* newDirectives = */ nullptr);
+    BINJS_TRY(funpc.init());
+    parseContext_->functionScope().useAsVarScope(parseContext_);
+    MOZ_ASSERT(parseContext_->isFunctionBox());
+
+    ParseContext::Scope lexicalScope(cx_, parseContext_, usedNames_);
+    BINJS_TRY(lexicalScope.init(parseContext_));
 
 
 
     MOZ_TRY(parseOptionalAssertedParameterScope());
 
 
 
 
     MOZ_TRY(parseOptionalAssertedVarScope());
 
 
 
 
     BINJS_MOZ_TRY_DECL(param, parseParameter());
 
 
-    BINJS_MOZ_TRY_DECL(funbox, buildFunctionBox(
-        GeneratorKind::NotGenerator,
-        FunctionAsyncKind::SyncFunction,
-        FunctionSyntaxKind::Setter, name));
-
-    // Push a new ParseContext. It will be used to parse `scope`, the arguments, the function.
-    BinParseContext funpc(cx_, this, funbox, /* newDirectives = */ nullptr);
-    BINJS_TRY(funpc.init());
-    parseContext_->functionScope().useAsVarScope(parseContext_);
-    MOZ_ASSERT(parseContext_->isFunctionBox());
-
-    ParseContext::Scope lexicalScope(cx_, parseContext_, usedNames_);
-    BINJS_TRY(lexicalScope.init(parseContext_));
+
 
     BINJS_MOZ_TRY_DECL(body, parseFunctionBody());
 
 
     ParseNode* params = new_<ListNode>(ParseNodeKind::ParamsBody, param->pn_pos);
     factory_.addList(params, param);
     BINJS_MOZ_TRY_DECL(method, buildFunction(start, kind, name, params, body, funbox));
     BINJS_TRY_DECL(result, factory_.newObjectMethodOrPropertyDefinition(name, method, AccessorType::Setter));
--- a/js/src/frontend/BinSource.webidl_
+++ b/js/src/frontend/BinSource.webidl_
@@ -437,18 +437,18 @@ interface EagerMethod : Node {
 };
 
 [Skippable] interface SkippableMethod : Node {
   attribute EagerMethod skipped;
 };
 
 // `get PropertyName ( ) { FunctionBody }`
 interface EagerGetter : Node {
+  attribute PropertyName name;
   attribute AssertedVarScope? bodyScope;
-  attribute PropertyName name;
   attribute FunctionBody body;
 };
 
 [Skippable] interface SkippableGetter : Node {
   attribute EagerGetter skipped;
 };
 
 // `set PropertyName ( PropertySetParameterList ) { FunctionBody }`
--- a/js/src/frontend/BinSource.yaml
+++ b/js/src/frontend/BinSource.yaml
@@ -556,22 +556,22 @@ EagerFunctionExpression:
                 BINJS_TRY(lexicalScope.init(parseContext_));
     build: |
         BINJS_TRY_DECL(lexicalScopeData, NewLexicalScopeData(cx_, lexicalScope, alloc_, parseContext_));
         BINJS_TRY_VAR(body, factory_.newLexicalScope(*lexicalScopeData, body));
         BINJS_MOZ_TRY_DECL(result, buildFunction(start, kind, name, params, body, funbox));
 
 EagerGetter:
     fields:
-        body:
-            before: |
+        name:
+            after: |
                 BINJS_MOZ_TRY_DECL(funbox, buildFunctionBox(
                     GeneratorKind::NotGenerator,
                     FunctionAsyncKind::SyncFunction,
-                    FunctionSyntaxKind::Getter, name));
+                    FunctionSyntaxKind::Getter, /* name = */ nullptr));
 
                 // Push a new ParseContext. It will be used to parse `scope`, the arguments, the function.
                 BinParseContext funpc(cx_, this, funbox, /* newDirectives = */ nullptr);
                 BINJS_TRY(funpc.init());
                 parseContext_->functionScope().useAsVarScope(parseContext_);
                 MOZ_ASSERT(parseContext_->isFunctionBox());
 
                 ParseContext::Scope lexicalScope(cx_, parseContext_, usedNames_);
@@ -586,22 +586,22 @@ EagerMethod:
         const auto syntax = FunctionSyntaxKind::Method;
     inherits: EagerFunctionExpression
     build: |
         BINJS_MOZ_TRY_DECL(method, buildFunction(start, kind, name, params, body, funbox));
         BINJS_TRY_DECL(result, factory_.newObjectMethodOrPropertyDefinition(name, method, AccessorType::None));
 
 EagerSetter:
     fields:
-        body:
-            before: |
+        name:
+            after: |
                 BINJS_MOZ_TRY_DECL(funbox, buildFunctionBox(
                     GeneratorKind::NotGenerator,
                     FunctionAsyncKind::SyncFunction,
-                    FunctionSyntaxKind::Setter, name));
+                    FunctionSyntaxKind::Setter, /* name = */ nullptr));
 
                 // Push a new ParseContext. It will be used to parse `scope`, the arguments, the function.
                 BinParseContext funpc(cx_, this, funbox, /* newDirectives = */ nullptr);
                 BINJS_TRY(funpc.init());
                 parseContext_->functionScope().useAsVarScope(parseContext_);
                 MOZ_ASSERT(parseContext_->isFunctionBox());
 
                 ParseContext::Scope lexicalScope(cx_, parseContext_, usedNames_);
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bdce15677836206b795e83fed4c39ae22e399589
GIT binary patch
literal 323
zc${62!AiqG5Qdkfgd(Mgh=iVsNKWDlcuHtmBQ{F5cnKb+$+T=ucEju{=9zp+N1+Bf
zm-(6hpKq8X%O8~}S4F&D$Hkkj(Rl6q`)@fD*NHZ@Hv8;vxYJ0{E0Bh5gA1%0An@9n
z=t+AdXkN&6NWRaZ#hcTdr6~RC2%X~%<-udi%!#T<cm6JgJ<{^SRKL6-H(y$68O(qd
z`GyM@;Nf`ZHg=9X*8je$*qJ|X4_Ntu5+1>ct@~1KiY#BL;nCSx9)eGxU}vp-FE>S+
e4(7u|2zhoTFM`pORd6W<*SX{t3OSN;%<2c*WNS45
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/argument_and_var.js
@@ -0,0 +1,6 @@
+// This should not cause `j` to be redeclared.
+({
+    foo: function k(j) {
+        var j
+    }
+});
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2e1d2772237eb94d3b900e51ef7cfa31ef8fd15b
GIT binary patch
literal 213
zc${5Mu?oU46bzvhDI&$eS&H}tZcc42r9wL-#icmJ^r^v^lr)O{X}`hHrQi+6amO8(
z6xETUCMR)O#^fwoP*&Js|6QUe5vmpHR*c?NfC&<*`D2wb%kQaVuO2ohW(2!PH~^Q-
zFsscv?t3s6S{G5SJBVsCj1_8`g{K?a{<nlybIOhAY(r~OR5_iVgq{Z<|IJKOlPsJ3
UT^JyY9J~d_GpFVrLOw>0e~Iiv-v9sr
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/catch_collision.js
@@ -0,0 +1,6 @@
+var ex;
+try {
+
+} catch (ex) {
+
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5b8e4577281dba0f86615e510e464eb827e34d36
GIT binary patch
literal 140
zc${<c^z#a4h;|Qh^!0TNip@+(%`3?)skE-+Vc>Gg$xqG}b51Nt&T!62EG<q|3Mr}t
zbAw9~OHy-zN|l^4^HMVN(mlcY(lS$vc!HCQG7CzggF}Km{oI2QCbBUxrACJYxw;~R
SWVjd@nAn&Y7+5$MfFuAAuq(>|
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/catch_implicit.js
@@ -0,0 +1,6 @@
+// `e` is neither a var, a let, a param, nor a free variable.
+try {
+
+} catch (e) {
+
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..84870012fafb8924628979a6b2af2cd85dafe94c
GIT binary patch
literal 326
zc${5N!AiqG5M7oKij*QEQly7cq<HENcuH(qB5jdu@e(|Y$+RqNcEjvg?05NtZUtFz
zPH*0u_a2j%yB8(ub(L+mS@o`4w5<93^eykjbmgc6^9lqVGLoNW-Xk%#Z$PSXJ<f7%
zTWyc!ukE0b76lxUR-d+E?QHSY6M7%eL$GN0Fu26Vgg|yQR1N8#$A8DUYtM^~4n<%T
zQ;3cB$Jh|b$uk%e*Lj4Bfw2pY`TyD=)@omsyS0kXPSOFMz@!J~(i2d<uZkk7%V8p<
X2$S61-UxXu$BDd3#o$6+gpm+Gn2c@C
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/eval_fake.js
@@ -0,0 +1,4 @@
+// This is not a direct call to `eval`.
+eval("foo");
+
+var eval = function() {}
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..37c5b243ddf95b16800687030bab3a2c0cf58dab
GIT binary patch
literal 279
zc$`H~O-sW-5FM6Mij-2MhzAeFgY+al*HfZtiP$39;wAJjB-604*$uO!*njUT$j<4#
znfJlu<>6IBy{oeQKC4cqB_~EMPo27fRr$xxhPe11m_jh#-EKc!BN61jz1-)<wZ?Tx
zxZHZ~(Y_;NPeL5-p3&N5Ul-UB|7W~VW25pHKE9bKN2`TnU@|-;s0D@k*tE<-AQoyr
zkagn+n(BtlAhkYL<zc7O8;j9|zR-@(-q**fDAHxKxCRArM)NBLYNn<D<47lQ1MmZr
CbzLF=
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/eval_fake_2.js
@@ -0,0 +1,4 @@
+// This is not a direct call to `eval`.
+eval("foo");
+
+function eval() {}
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fd7647f4f708f4a03d5478570b87b002737f2027
GIT binary patch
literal 312
zc$`H~!D_-l5FHm0DN;(2;-LreP<oKkdrwIlB~nAlikHyCl8myj*$uPPQa_~s+ts2w
zr}t*w%zKmO#Y#f8ER*#*DL-|CmNg$<e#K;(c#p)`Y!%7cbvR44ZM5CzC(%wLJw}-Q
zbcEh(XP+~*M_PQ@nge^o+9*;-I9)humH7y?MMm-?S(EPgA?`|6jBU6%Kbv3FrwC8)
zIzVM4QwS3G?q8KLkxtI;scQ~U$xWFTOBuf#_lNkc%wXqSwJEbKPKql4A)p6ha0J%^
S{;M|(-McY}(QO~ZjrarlH)e_e
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/eval_fake_in_function.js
@@ -0,0 +1,4 @@
+// This is not a direct call to eval.
+function foo(eval) {
+    eval("foo");
+}
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..98f67d4c931fcd0f9154b4efb6306d6a2d8923df
GIT binary patch
literal 309
zc$`H~!D<3A5FLYvh!iPOJa~{Eq=!<x_q5xtc1so6NH0qdVRw{3Hd!XqQa_~sYZPqe
z^muRHyf<mSd6!VGie$Y`iZ5NGWzEOuKQWml-Xk&ApNeGVT71Ya>Ruydzb&En+S%FC
zQKQUOk!p~U{5)!s6=M@T_o=qEwugA&=|hKQWh7JJqh%;7(k)M4lrfQo$@1G)98l`&
z<04ZBq$~4^l?mZz_;_vn-{#W0l)EC|tYrLd-0kh1>*J3y<*vxGsJsmU1V9JG7@!Y#
RXBei_h0!gD-i;7q04L16W_tht
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/eval_in_function.js
@@ -0,0 +1,4 @@
+// This is a direct call to eval, in foo.
+function foo() {
+    eval("foo");
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cf01c1287642bf3881ac41a9bc778ca84665bc06
GIT binary patch
literal 190
zc${<c^z#a4h;|Qh^!0TNip@+(%`3?)skFYtqu~i=r)8!VxmFYur4|=w=I5y)n8789
zC8@bUB{I&5IXMV%p5Wx7%z_d<pUjffqQsowlA_GKbcD2mV{vh6QAuh_SYlCda(+Q-
wbZ|(Jr=NQ;!fp{3j?}Wm9JaLl{OFJ%S676L92*w{0|O%i6B7#q0}}@m04sz*@c;k-
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/eval_simple.js
@@ -0,0 +1,2 @@
+// This direct call to `eval` must be detected.
+eval("foo");
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7b34270ea36c1ec8859f25173ac38f69ffd2ad6e
GIT binary patch
literal 305
zc${62!Ab)$5Qc|PibyFUB0YF0B0Y+C53;M<vc)cCQ@ku5#_U)E*<_idVxPg6?IV~L
zB;XwW!^ijkFnL)mB{YjVTdlJCUAJU|4*SO+F@f7$+g96cd4ivsc$#@n9H@Ommdf??
z@Tvnbnrhb(YgK&rOx|l}M^drw!*uSrL$gN4j-u&jNs$2qb#eOU8I4JN7RNPy7<wUP
zVlhN^_n$ewFK|md-&u7OQhoA=a@Xz~xv9%)A=6{yfxzJxa<i$6BH{HJfEbOhVgd|~
T0p|eMATETs6flI-9iHG9F$82E
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/function_expression.js
@@ -0,0 +1,4 @@
+// This should not cause `a` to be redeclared.
+(function a() {
+    var a = 1;
+})()
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bd9cc8357d181e7306b8728f110f56fe86ca310e
GIT binary patch
literal 388
zc${62K}*9x5QT>&EG2{@BBh9jAkvcu@17D&L&V0C6feQUCYhEkX*O(DY5tRc!T;tY
zwnV%v-@Nzc?XDli&naYK;;mL*@-9oPjWpfkH@ms@WnId87Y*80qISKuMQyNr6FMza
zg9`#F@(Q1@sDu_qs(QZssEZzaRd#p(14_gFxn)hl0S%tIUL>KY)|v)wx|lroHQ!@l
zf@7nxZK=~;Dt(~Ld9!^8#18fO%0JI=(n8C|%mzP1L*ZXe2hP$>62)OU{&?Xq<ex&b
zj&GIXD&MnXwn>6ujC{=)Fb)9hiVp6u9)V2&E(vW8%wjZ6F0A>~CZi9XG_bFyw!^Gp
JfpoA&=LfJ)aK8Wm
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/getter_setter_scope.js
@@ -0,0 +1,10 @@
+function foo(e) {
+    var obj = {
+        get x() {
+            var e; // This `e` and the function argument `e` should have a distinct scope.
+        },
+        set x(y) {
+            var e; // This `e` and the function argument `e` should have a distinct scope.
+        }
+    };
+}
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6036f4357c6ea20fff544ec64dfc2eb2015c8cab
GIT binary patch
literal 282
zc${61K}*9x5QT>&gi?x#c<>lRJooNF5==wH2FV65!NZ!2Wni-#W~X5PwLihNSPIT%
z-n{wVFh$)wYv`*s-){5vokny*a(JlJHDq}R$b#dWVLkX6ZwqoGxm|svJ_$M7c%F=X
zHEbpn<gk9B*o<xV6EY3u*UT6KdABaj4tcY8gT(0x2OBfL@-AK{^I`Gid0y#(X_lK4
zUHI{E{~wEfue-KxDt&%COP7`=l7(Q9)S~oVTbAd2ccDNj08mSS<WEBia0%)vQL6+N
IaIEjgAB417zyJUM
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/let_in_fun.js
@@ -0,0 +1,3 @@
+function test() {
+    let limit = 1;
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c1ae09025427e40ad662ee318ef28db51685e5f9
GIT binary patch
literal 348
zc${62K}*9x5QUc|gb;#=h<GeTibwArY)WH^*eKcJC3x8GPRvM}4YOOZKf!<NPjJzc
zfbaBXc<&o#krxkwRjV>xuha4sHFOT$;r5GP+&rQKl5Mfy0inu1djf0GbeBu%<TIHb
z$Q@=w>Npp22b0uV??Jyv63Xwd^IvX_KqF=<8bhv;oj!IaPuq7;Zj4-c0LpC(OHi#O
z=?qO5*lFzEMq(lqVee-Dv%X)6O_>)fG5&R*gtakAZT9|jTq9YRSWu?Z;fJKyHAuSJ
nlvy@b`m>O6=Ect=esao^hz9}t)At<@!ojmS58}Z_^El!^pH6S9
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/object_literal.js
@@ -0,0 +1,6 @@
+var foo = {
+    'first': 1,
+    second: 2,
+    "third": 3,
+    fourth
+};
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4d5cdd51f3ab414893dc46a6f46fc3e4537cc898
GIT binary patch
literal 180
zc${<c^z#a4h;|Qh^!0TNip@+(%`3?)skC0k!=U7pnU|88m+lD`OUq0xQVB~e%1lhk
zNp(q0&PgmvEXglYLl?@-&r@(LE>0~fNlgLD1t;ehr1AtO7iAWdL<ffidHT5rBWw}l
uU`l36WJ+QyODu{G337EsNSSbPFt9PPFaQA)8yg!76B7dqkjuivzyJURk2aJ5
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/toplevel_var.js
@@ -0,0 +1,1 @@
+var a, b, c;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2073a514fa6d4c33364c6a8d69996424bcb7d0b6
GIT binary patch
literal 282
zc${61K}*9x5QT>&gi?x#c<>lRJony%B$$SX4U!FBLJ#X^ECZX}FuN7|ul)&b3)13)
zfnmP)9#hoKi-f*v^UWr2w=|#)6b`FO+-AA=$N`6U#ZtQo;l3a{klocM*C!z_q-L5z
zwst%!^QKsh7?AyA%#ivp_>9`bYanNruN09o&3;5CU4BoD-bZk^-dlZi6mB>?{7*Vw
zmTHImXS!(YW!KhCCC_hX=`5fRb3-GA^h<HywPktcT?!x|O56Y>$5q6cfw%%OPsDX1
J7I4zf(=TTKS;PPU
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/var_in_fun.js
@@ -0,0 +1,3 @@
+function test() {
+    var limit = 1;
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c8d1a4ebe0d2d91cc6914990c49d7f6a5ca13d97
GIT binary patch
literal 315
zc${62!AiqG5QdkfgiuNm5%E|=qz4bZdyoXv60t$D#Y+%eH)9ys?1tHm*thl(+yr95
zIm`^-{PQ!D=Eaj>)viqT`=ors8jMGOyzlwtb&94&)8r%jM4(mT9FT|lRgzG)gH0wI
zplxrIM{6EIX-RTuZ-N5lhlYt+S_nnh`PJ4^EA>l&EqEZuZvSh;{?+7}+WNS8#vqis
z=vyFE*>^|aTo~R>_x!nyS5@Lr=EY8&zFEhy2j}M+TlA5nCi;?89m*^_Y2IVbI19mN
aL%tYFAOX!en=Q^5Uq(D){G5&P^Y9DJt7K{b
new file mode 100644
--- /dev/null
+++ b/js/src/jsapi-tests/binast/parser/multipart/unit/var_in_fun_plus_arg.js
@@ -0,0 +1,3 @@
+function test(arg) {
+    var limit = 1;
+}
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Basic transparent rendering</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: true });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Setting the canvas size should clear the canvas</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: true });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+c.width = 200;
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-3.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Basic rendering into a non-alpha canvas</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: false });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-4.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Setting the canvas size on a non-alpha should clear the canvas to opaque black</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: false });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+c.width = 200;
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-5.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Only the context attributes from the first getContext call should be respected.</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: true });
+ctx = c.getContext('2d', { alpha: false });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-6.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: moz-opaque should have the same effect as alpha:false</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200" moz-opaque="true"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: true });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-7.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Unsetting moz-opaque should clear the canvas</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200" moz-opaque="true"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: true });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+c.removeAttribute("moz-opaque");
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-8.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Unsetting moz-opaque should clear the canvas even if the canvas has been created with alpha:false, and the canvas should stay opaque.</title>
+
+<div style="width: 200px; height: 200px; background-color: red;">
+  <canvas id="c" width="200" height="200" moz-opaque="true"></canvas>
+</div>
+
+<script>
+
+var c = document.getElementById('c');
+var ctx = c.getContext('2d', { alpha: false });
+ctx.fillStyle = 'green';
+ctx.fillRect(50, 50, 100, 100);
+c.removeAttribute("moz-opaque");
+
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-ref-opaque-clear.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Reference for an opaque canvas with nothing rendered in it</title>
+
+<div style="width: 200px; height: 200px; background: black"></div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-ref-opaque-with-rendering.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Reference for an opaque canvas with a green square rendered in it</title>
+
+<div style="width: 200px; height: 200px; background: black; display: flex;">
+  <div style="width: 100px; height: 100px; margin: auto; background: green;"></div>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-ref-transparent-clear.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Reference for a regular (non-opaque) canvas with nothing rendered in it. The red background behind the canvas is visible.</title>
+
+<div style="width: 200px; height: 200px; background: red"></div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/bugs/1219985-ref-transparent-with-rendering.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Bug 1219985: Reference for a regular (non-opaque) canvas with a green square rendered in it. The red background behind the canvas is visible.</title>
+
+<div style="width: 200px; height: 200px; background: red; display: flex;">
+  <div style="width: 100px; height: 100px; margin: auto; background: green;"></div>
+</div>
--- a/layout/reftests/bugs/reftest.list
+++ b/layout/reftests/bugs/reftest.list
@@ -1954,16 +1954,24 @@ fuzzy(1,74) fails-if(Android||gtkWidget)
 == 1202512-1.html 1202512-1-ref.html
 fuzzy-if(skiaContent,1,1) == 1202512-2.html 1202512-2-ref.html
 != 1207326-1.html about:blank
 == 1209603-1.html 1209603-1-ref.html
 == 1209994-1.html 1209994-1-ref.html
 == 1209994-2.html 1209994-2-ref.html
 == 1209994-3.html 1209994-3-ref.html
 == 1209994-4.html 1209994-4-ref.html
+== 1219985-1.html 1219985-ref-transparent-with-rendering.html
+== 1219985-2.html 1219985-ref-transparent-clear.html
+== 1219985-3.html 1219985-ref-opaque-with-rendering.html
+== 1219985-4.html 1219985-ref-opaque-clear.html
+== 1219985-5.html 1219985-ref-transparent-with-rendering.html
+== 1219985-6.html 1219985-ref-opaque-with-rendering.html
+== 1219985-7.html 1219985-ref-transparent-clear.html
+== 1219985-8.html 1219985-ref-opaque-clear.html
 == 1222226-1.html 1222226-1-ref.html
 pref(layout.css.overflow-clip-box.enabled,true) == 1226278.html 1226278-ref.html
 == 1230466.html about:blank
 random-if(gtkWidget) != 1238243-1.html 1238243-1-notref.html # may fail on Linux, depending on Korean fonts available
 == 1238243-2.html 1238243-2-ref.html
 fuzzy(100,2000) == 1239564.html 1239564-ref.html
 == 1242172-1.html 1242172-1-ref.html
 fuzzy-if(webrender,0-2,0-2601) == 1242172-2.html 1242172-2-ref.html
--- a/layout/reftests/native-theme/reftest.list
+++ b/layout/reftests/native-theme/reftest.list
@@ -48,20 +48,23 @@ fails-if(Android&&!asyncPan) != 492155-4
 
 # RTL mirroring tests
 == checkbox-not-mirrored-when-rtl.html checkbox-not-mirrored-when-rtl-ref.html
 skip-if(!cocoaWidget) == menulist-mirrored-when-rtl.xul menulist-mirrored-when-rtl-ref.xul
 skip-if(!cocoaWidget) == searchfield-mirrored-when-rtl.xul searchfield-mirrored-when-rtl-ref.xul
 skip-if(!cocoaWidget) == select-mirrored-when-rtl.html select-mirrored-when-rtl-ref.html
 
 != resizer-bottomend.xul blank-window.xul
-random-if(d2d) == resizer-bottomend.xul resizer-bottomright.xul # bug 581086 
+random-if(d2d) == resizer-bottomend.xul resizer-bottomright.xul # bug 581086
 != resizer-bottomend.xul resizer-bottomend-rtl.xul
+# Disabled on Linux (bug 519152), random on Windows (bug 581086)
 skip-if(gtkWidget) != resizer-bottomend-rtl.xul blank-window.xul
-skip-if(gtkWidget) random-if(d2d) == resizer-bottomend-rtl.xul resizer-bottomend-flipped.xul # bug 581086
+skip-if(gtkWidget) random-if(d2d) == resizer-bottomend-rtl.xul resizer-bottomend-flipped.xul
+skip-if(gtkWidget) == resizer-bottomleft-rtl.xul resizer-bottomleft.xul
+skip-if(gtkWidget) == resizer-bottomright-rtl.xul resizer-bottomright.xul
 
 != resizer-bottomstart.xul blank-window.xul
 random-if(d2d) == resizer-bottomstart.xul resizer-bottomleft.xul
 random-if(d2d) == resizer-bottomstart.xul resizer-left.xul
 != resizer-bottomstart.xul resizer-bottomstart-rtl.xul
 skip-if(gtkWidget) != resizer-bottomstart-rtl.xul blank-window.xul
 skip-if(gtkWidget) random-if(d2d) == resizer-bottomstart-rtl.xul resizer-bottomend.xul
 
copy from layout/reftests/native-theme/resizer-bottomleft.xul
copy to layout/reftests/native-theme/resizer-bottomleft-rtl.xul
--- a/layout/reftests/native-theme/resizer-bottomleft.xul
+++ b/layout/reftests/native-theme/resizer-bottomleft-rtl.xul
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window title="Resizer"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" localedir="rtl">
   <hbox>
+    <spacer flex="1"/>
     <resizer dir="bottomleft"/>
-    <spacer flex="1"/>
   </hbox>
 </window>
copy from layout/reftests/native-theme/resizer-bottomright.xul
copy to layout/reftests/native-theme/resizer-bottomright-rtl.xul
--- a/layout/reftests/native-theme/resizer-bottomright.xul
+++ b/layout/reftests/native-theme/resizer-bottomright-rtl.xul
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window title="Resizer"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" localedir="rtl">
   <hbox>
+    <spacer flex="1"/>
     <resizer dir="bottomright"/>
-    <spacer flex="1"/>
   </hbox>
 </window>
--- a/layout/reftests/xul/reftest.list
+++ b/layout/reftests/xul/reftest.list
@@ -83,16 +83,18 @@ test-pref(svg.context-properties.content
 
 != resizer-bottomend.xul blank-window.xul
 == resizer-bottomend.xul resizer-bottomright.xul
 != resizer-bottomend.xul resizer-bottomend-rtl.xul
 != resizer-bottomend-rtl.xul blank-window.xul
 # fuzzy for comparing SVG image flipped by CSS with a flipped SVG image.
 # See bug 1450017 comment 79.
 fuzzy(43,98) == resizer-bottomend-rtl.xul resizer-bottomend-flipped.xul
+fuzzy(43,98) == resizer-bottomleft-rtl.xul resizer-bottomleft.xul
+fuzzy(43,98) == resizer-bottomright-rtl.xul resizer-bottomright.xul
 
 != resizer-bottomstart.xul blank-window.xul
 == resizer-bottomstart.xul resizer-bottomleft.xul
 == resizer-bottomstart.xul resizer-left.xul
 != resizer-bottomstart.xul resizer-bottomstart-rtl.xul
 != resizer-bottomstart-rtl.xul blank-window.xul
 # fuzzy for comparing SVG image flipped by CSS to a flipped SVG image.
 # See bug 1450017 comment 79.
copy from layout/reftests/xul/resizer-bottomleft.xul
copy to layout/reftests/xul/resizer-bottomleft-rtl.xul
--- a/layout/reftests/xul/resizer-bottomleft.xul
+++ b/layout/reftests/xul/resizer-bottomleft-rtl.xul
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window title="Resizer"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" localedir="rtl">
   <hbox>
+    <spacer flex="1"/>
     <resizer dir="bottomleft" style="-moz-appearance: none"/>
-    <spacer flex="1"/>
   </hbox>
 </window>
copy from layout/reftests/xul/resizer-bottomright.xul
copy to layout/reftests/xul/resizer-bottomright-rtl.xul
--- a/layout/reftests/xul/resizer-bottomright.xul
+++ b/layout/reftests/xul/resizer-bottomright-rtl.xul
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 
 <window title="Resizer"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" localedir="rtl">
   <hbox>
+    <spacer flex="1"/>
     <resizer dir="bottomright" style="-moz-appearance: none"/>
-    <spacer flex="1"/>
   </hbox>
 </window>
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -48,30 +48,24 @@ support-files = additional_sheets_helper
 [test_all_shorthand.html]
 [test_animations.html]
 skip-if = (toolkit == 'android')
 [test_animations_async_tests.html]
 support-files = Ahem.ttf file_animations_async_tests.html
 [test_animations_dynamic_changes.html]
 [test_animations_effect_timing_duration.html]
 [test_animations_effect_timing_enddelay.html]
-skip-if = webrender # bug 1424752
 [test_animations_effect_timing_iterations.html]
-skip-if = webrender # bug 1424752
 [test_animations_event_order.html]
 [test_animations_event_handler_attribute.html]
 [test_animations_iterationstart.html]
-skip-if = webrender # bug 1424752
 [test_animations_omta.html]
-skip-if = webrender # bug 1424752
 [test_animations_omta_start.html]
 [test_animations_pausing.html]
-skip-if = webrender # bug 1424752
 [test_animations_playbackrate.html]
-skip-if = webrender # bug 1424752
 [test_animations_reverse.html]
 [test_animations_styles_on_event.html]
 [test_animations_variable_changes.html]
 [test_animations_with_disabled_properties.html]
 support-files = file_animations_with_disabled_properties.html
 [test_any_dynamic.html]
 [test_asyncopen2.html]
 [test_at_rule_parse_serialize.html]
--- a/layout/style/test/test_animations_omta.html
+++ b/layout/style/test/test_animations_omta.html
@@ -176,16 +176,19 @@ https://bugzilla.mozilla.org/show_bug.cg
 <a target="_blank"
   href="https://bugzilla.mozilla.org/show_bug.cgi?id=964646">Mozilla Bug
   964646</a>
 <div id="display"></div>
 <pre id="test">
 <script type="application/javascript">
 "use strict";
 
+const isWebRender =
+  SpecialPowers.DOMWindowUtils.layerManagerType == 'WebRender';
+
 /** Test for css3-animations running on the compositor thread (Bug 964646) **/
  
 // Global state
 var gDisplay = document.getElementById("display")
   , gDiv     = null;
 
 // Shortcut omta_is and friends by filling in the initial 'elem' argument
 // with gDiv.
@@ -332,19 +335,23 @@ async function testFillMode(fillMode, fi
             desc + "affects value after animation");
   else
     omta_is("transform", { tx: 30 }, RunningOn.MainThread,
             desc + "does not affect value after animation");
 
   done_div();
 }
 
-addAsyncAnimTest(function() { return testFillMode("", false, false); });
-addAsyncAnimTest(function() { return testFillMode("none", false, false); });
-addAsyncAnimTest(function() { return testFillMode("forwards", false, true); });
+// FIXME: Bug 1456679: On WebRender these test cases incorrectly get style value
+// on the compositor when the animation is in-delay phase.
+if (!isWebRender) {
+  addAsyncAnimTest(function() { return testFillMode("", false, false); });
+  addAsyncAnimTest(function() { return testFillMode("none", false, false); });
+  addAsyncAnimTest(function() { return testFillMode("forwards", false, true); });
+}
 addAsyncAnimTest(function() { return testFillMode("backwards", true, false); });
 addAsyncAnimTest(function() { return testFillMode("both", true, true); });
 
 // Test that animations continue running when the animation name
 // list is changed.
 //
 // test_animations.html combines all these tests into one block but this is
 // difficult for OMTA because currently there are only two properties to which
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -391,16 +391,25 @@
 
         <!-- DON'T EXPORT THIS, please! An attacker could delete arbitrary files. -->
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.cleanup.FileCleanupService">
         </service>
 
         <receiver
+            android:name="org.mozilla.gecko.mma.PackageAddedReceiver"
+            android:exported="false">
+            <intent-filter>
+                 <action android:name="android.intent.action.PACKAGE_ADDED"/>
+                 <data android:scheme="package"/>
+            </intent-filter>
+        </receiver>
+
+        <receiver
             android:name="org.mozilla.gecko.PackageReplacedReceiver"
             android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.MY_PACKAGE_REPLACED"></action>
             </intent-filter>
         </receiver>
 
         <service
--- a/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/MmaDelegate.java
@@ -39,33 +39,36 @@ public class MmaDelegate {
     public static final String READER_AVAILABLE = "E_Reader_Available";
     public static final String DOWNLOAD_MEDIA_SAVED_IMAGE = "E_Download_Media_Saved_Image";
     public static final String CLEARED_PRIVATE_DATA = "E_Cleared_Private_Data";
     public static final String SAVED_BOOKMARK = "E_Saved_Bookmark";
     public static final String OPENED_BOOKMARK = "E_Opened_Bookmark";
     public static final String INTERACT_WITH_SEARCH_URL_AREA = "E_Interact_With_Search_URL_Area";
     public static final String SCREENSHOT = "E_Screenshot";
     public static final String SAVED_LOGIN_AND_PASSWORD = "E_Saved_Login_And_Password";
-    public static final String LAUNCH_BUT_NOT_DEFAULT_BROWSER = "E_Launch_But_Not_Default_Browser";
-    public static final String LAUNCH_BROWSER = "E_Launch_Browser";
     public static final String RESUMED_FROM_BACKGROUND = "E_Resumed_From_Background";
     public static final String NEW_TAB = "E_Opened_New_Tab";
     public static final String DISMISS_ONBOARDING = "E_Dismiss_Onboarding";
-    public static final String CHANGED_DEFAULT_TO_FENNEC = "E_Changed_Default_To_Fennec";
+
+    private static final String LAUNCH_BUT_NOT_DEFAULT_BROWSER = "E_Launch_But_Not_Default_Browser";
+    private static final String LAUNCH_BROWSER = "E_Launch_Browser";
+    private static final String CHANGED_DEFAULT_TO_FENNEC = "E_Changed_Default_To_Fennec";
+    private static final String INSTALLED_FOCUS = "E_Just_Installed_Focus";
+    private static final String INSTALLED_KLAR = "E_Just_Installed_Klar";
 
-    public static final String USER_ATT_FOCUS_INSTALLED = "Focus Installed";
-    public static final String USER_ATT_KLAR_INSTALLED = "Klar Installed";
-    public static final String USER_ATT_POCKET_INSTALLED = "Pocket Installed";
-    public static final String USER_ATT_DEFAULT_BROWSER = "Default Browser";
-    public static final String USER_ATT_SIGNED_IN = "Signed In Sync";
-    public static final String USER_ATT_POCKET_TOP_SITES = "Pocket in Top Sites";
+    private static final String USER_ATT_FOCUS_INSTALLED = "Focus Installed";
+    private static final String USER_ATT_KLAR_INSTALLED = "Klar Installed";
+    private static final String USER_ATT_POCKET_INSTALLED = "Pocket Installed";
+    private static final String USER_ATT_DEFAULT_BROWSER = "Default Browser";
+    private static final String USER_ATT_SIGNED_IN = "Signed In Sync";
+    private static final String USER_ATT_POCKET_TOP_SITES = "Pocket in Top Sites";
 
-    public static final String PACKAGE_NAME_KLAR = "org.mozilla.klar";
-    public static final String PACKAGE_NAME_FOCUS = "org.mozilla.focus";
-    public static final String PACKAGE_NAME_POCKET = "com.ideashower.readitlater.pro";
+    private static final String PACKAGE_NAME_KLAR = "org.mozilla.klar";
+    private static final String PACKAGE_NAME_FOCUS = "org.mozilla.focus";
+    private static final String PACKAGE_NAME_POCKET = "com.ideashower.readitlater.pro";
 
     private static final String TAG = "MmaDelegate";
 
     public static final String KEY_ANDROID_PREF_STRING_LEANPLUM_DEVICE_ID = "android.not_a_preference.leanplum.device_id";
     private static final String KEY_ANDROID_PREF_BOOLEAN_FENNEC_IS_DEFAULT = "android.not_a_preference.fennec.default.browser.status";
 
     private static final String DEBUG_LEANPLUM_DEVICE_ID = "8effda84-99df-11e7-abc4-cec278b6b50a";
 
@@ -126,16 +129,30 @@ public class MmaDelegate {
             if (!sharedPreferences.getBoolean(KEY_ANDROID_PREF_BOOLEAN_FENNEC_IS_DEFAULT, true) && isFennecDefaultBrowser) {
                 track(CHANGED_DEFAULT_TO_FENNEC);
             }
         }
 
         sharedPreferences.edit().putBoolean(KEY_ANDROID_PREF_BOOLEAN_FENNEC_IS_DEFAULT, isFennecDefaultBrowser).apply();
     }
 
+    static void trackJustInstalledPackage(@NonNull final Context context, @NonNull final String packageName,
+                                          final boolean firstTimeInstall) {
+        if (!isMmaEnabled(context)) {
+            return;
+        }
+
+        if (packageName.equals(PACKAGE_NAME_FOCUS) && firstTimeInstall) {
+            // Already know Mma is enabled, safe to call directly and avoid a superfluous check
+            mmaHelper.event(INSTALLED_FOCUS);
+        } else if (packageName.equals(PACKAGE_NAME_KLAR) && firstTimeInstall) {
+            mmaHelper.event(INSTALLED_KLAR);
+        }
+    }
+
     public static void track(String event) {
         if (applicationContext != null && isMmaEnabled(applicationContext)) {
             mmaHelper.event(event);
         }
     }
 
 
     public static void track(String event, long value) {
@@ -157,17 +174,16 @@ public class MmaDelegate {
         final Tab selectedTab = Tabs.getInstance().getSelectedTab();
 
         // if selected tab is private, mma should be disabled.
         final boolean isInPrivateBrowsing = selectedTab != null && selectedTab.isPrivate();
         // only check Gecko Pref when Gecko is running
         return inExperiment && healthReport && !isInPrivateBrowsing;
     }
 
-
     public static boolean isDefaultBrowser(Context context) {
         final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.mozilla.org"));
         final ResolveInfo info = context.getPackageManager().resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
         if (info == null) {
             // No default is set
             return false;
         }
         final String packageName = info.activityInfo.packageName;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/mma/PackageAddedReceiver.java
@@ -0,0 +1,43 @@
+package org.mozilla.gecko.mma;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+
+
+/**
+ * Used to inform as soon as possible of any applications being installed.
+ */
+public class PackageAddedReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (intent.getAction() != null && intent.getData() != null) {
+            final String updatedPackage = getInstalledPackageName(intent.getData());
+            try {
+                MmaDelegate.trackJustInstalledPackage(context, updatedPackage,
+                        getIfFirstTimeInstall(context, updatedPackage));
+            } catch (PackageManager.NameNotFoundException e) {
+                /* Nothing to do */
+            }
+        }
+    }
+
+    // Our intent filter uses the "package" scheme
+    // So the intent we receive would be in the form package:org.mozilla.klar
+    private String getInstalledPackageName(@NonNull Uri intentData) {
+        return intentData.getSchemeSpecificPart();
+    }
+
+    private boolean getIfFirstTimeInstall(@NonNull Context context, @NonNull final String packageName)
+            throws PackageManager.NameNotFoundException {
+
+        // The situation of an invalid package name(very unlikely) will be handled by the caller
+        final PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, 0);
+        return packageInfo.firstInstallTime == packageInfo.lastUpdateTime;
+    }
+}
--- a/mobile/android/components/BrowserCLH.js
+++ b/mobile/android/components/BrowserCLH.js
@@ -193,17 +193,17 @@ BrowserCLH.prototype = {
       this.LoginManagerContent.onDOMInputPasswordAdded(event, event.target.ownerGlobal.top);
     }, options);
 
     aWindow.addEventListener("DOMAutoComplete", event => {
       this.LoginManagerContent.onUsernameInput(event);
     }, options);
 
     aWindow.addEventListener("blur", event => {
-      if (event.target instanceof Ci.nsIDOMHTMLInputElement) {
+      if (ChromeUtils.getClassName(event.target) === "HTMLInputElement") {
         this.LoginManagerContent.onUsernameInput(event);
       }
     }, options);
 
     aWindow.addEventListener("pageshow", event => {
       // XXXbz what about non-HTML documents??
       if (ChromeUtils.getClassName(event.target) == "HTMLDocument") {
         this.LoginManagerContent.onPageShow(event, event.target.defaultView.top);
--- a/mobile/android/docs/mma.rst
+++ b/mobile/android/docs/mma.rst
@@ -159,16 +159,24 @@ List of current Events related data that
 * The user just resumed the app from background
 {
   "event" : "E_Resumed_From_Background"
 }
 * User set Fennec as default browser and resumed the app
 {
   "event" : "E_Changed_Default_To_Fennec"
 }
+* User installed the Focus app
+{
+  "event" : "E_Just_Installed_Focus"
+}
+* User installed the Klar app
+{
+  "event" : "E_Just_Installed_Klar"
+}
 
 Deep Links:
 Deep links are actions that can point Fennec to open certain pages or load features such as `show bookmark list` or
 `open a SUMO page`. When users see a prompt Leanplum message, they can click the button(s) on it. These buttons can
 trigger the following deep links
 * Link to Set Default Browser settings (firefox://default_browser)
 * Link to specific Add-on page (http://link_to_the_add_on_page)
 * Link to sync signup/sign in (firefox://sign_up)
--- a/mobile/android/geckoview/build.gradle
+++ b/mobile/android/geckoview/build.gradle
@@ -169,16 +169,20 @@ tasks.withType(org.jetbrains.kotlin.grad
             }
             def (_, type, file, line, column, message) = matches[0]
             type = (type == 'w') ? 'warning' : 'error'
             // Use logger.lifecycle, which does not go through stderr again.
             logger.lifecycle "$file:$line:$column: $type: $message"
         }
     } as StandardOutputListener
 
+    kotlinOptions {
+        allWarningsAsErrors = true
+    }
+
     doFirst {
         logging.addStandardErrorListener(listener)
     }
     doLast {
         logging.removeStandardErrorListener(listener)
     }
 }
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/GeckoSessionTestRuleTest.kt
@@ -313,17 +313,17 @@ class GeckoSessionTestRuleTest : BaseSes
 
     @Test fun waitUntilCalled_currentCall() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.session.reload()
 
         var counter = 0
 
         sessionRule.waitUntilCalled(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 2, order = intArrayOf(1, 2))
+            @AssertCalled(count = 2, order = [1, 2])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 val info = sessionRule.currentCall
                 assertThat("Method info should be valid", info, notNullValue())
                 assertThat("Counter should be correct",
                            info.counter, equalTo(forEachCall(1, 2)))
                 assertThat("Order should equal counter",
                            info.order, equalTo(info.counter))
                 counter++
@@ -458,70 +458,70 @@ class GeckoSessionTestRuleTest : BaseSes
         })
     }
 
     @Test fun forCallbacksDuringWait_specificOrder() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(order = intArrayOf(1))
+            @AssertCalled(order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
             }
 
-            @AssertCalled(order = intArrayOf(2))
+            @AssertCalled(order = [2])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
             }
         })
     }
 
     @Test(expected = AssertionError::class)
     fun forCallbacksDuringWait_throwOnWrongOrder() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(order = intArrayOf(2))
+            @AssertCalled(order = [2])
             override fun onPageStart(session: GeckoSession, url: String) {
             }
 
-            @AssertCalled(order = intArrayOf(1))
+            @AssertCalled(order = [1])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
             }
         })
     }
 
     @Test fun forCallbacksDuringWait_multipleOrder() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.session.reload()
         sessionRule.waitForPageStops(2)
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(order = intArrayOf(1, 3, 1))
+            @AssertCalled(order = [1, 3, 1])
             override fun onPageStart(session: GeckoSession, url: String) {
             }
 
-            @AssertCalled(order = intArrayOf(2, 4, 1))
+            @AssertCalled(order = [2, 4, 1])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
             }
         })
     }
 
     @Test(expected = AssertionError::class)
     fun forCallbacksDuringWait_throwOnWrongMultipleOrder() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.session.reload()
         sessionRule.waitForPageStops(2)
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(order = intArrayOf(1, 2, 1))
+            @AssertCalled(order = [1, 2, 1])
             override fun onPageStart(session: GeckoSession, url: String) {
             }
 
-            @AssertCalled(order = intArrayOf(3, 4, 1))
+            @AssertCalled(order = [3, 4, 1])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
             }
         })
     }
 
     @Test fun forCallbacksDuringWait_notCalled() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
@@ -660,22 +660,22 @@ class GeckoSessionTestRuleTest : BaseSes
     fun getCurrentCall_throwOnNoCurrentCall() {
         sessionRule.currentCall
     }
 
     @Test fun delegateUntilTestEnd() {
         var counter = 0
 
         sessionRule.delegateUntilTestEnd(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 counter++
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 counter++
             }
         })
 
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
@@ -704,21 +704,21 @@ class GeckoSessionTestRuleTest : BaseSes
 
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
     }
 
     @Test(expected = AssertionError::class)
     fun delegateUntilTestEnd_throwOnWrongOrder() {
         sessionRule.delegateUntilTestEnd(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onPageStart(session: GeckoSession, url: String) {
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
             }
         })
 
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
     }
 
@@ -738,22 +738,22 @@ class GeckoSessionTestRuleTest : BaseSes
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
     }
 
     @Test fun delegateDuringNextWait() {
         var counter = 0
 
         sessionRule.delegateDuringNextWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 counter++
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 counter++
             }
         })
 
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
@@ -781,44 +781,44 @@ class GeckoSessionTestRuleTest : BaseSes
     }
 
     @Test fun delegateDuringNextWait_hasPrecedence() {
         var testCounter = 0
         var waitCounter = 0
 
         sessionRule.delegateUntilTestEnd(object : Callbacks.ProgressDelegate,
                                                   Callbacks.NavigationDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onPageStart(session: GeckoSession, url: String) {
                 testCounter++
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(4))
+            @AssertCalled(count = 1, order = [4])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 testCounter++
             }
 
-            @AssertCalled(count = 2, order = intArrayOf(1, 3))
+            @AssertCalled(count = 2, order = [1, 3])
             override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
                 testCounter++
             }
 
-            @AssertCalled(count = 2, order = intArrayOf(1, 3))
+            @AssertCalled(count = 2, order = [1, 3])
             override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
                 testCounter++
             }
         })
 
         sessionRule.delegateDuringNextWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 waitCounter++
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 waitCounter++
             }
         })
 
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/NavigationDelegateTest.kt
@@ -22,44 +22,44 @@ import org.junit.runner.RunWith
 @MediumTest
 class NavigationDelegateTest : BaseSessionTest() {
 
     @Test fun load() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
                                        where: Int,
                                        flags: Int,
                                        response: GeckoResponse<Boolean>) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URI should not be null", uri, notNullValue())
                 assertThat("URI should match", uri, endsWith(HELLO_HTML_PATH))
                 assertThat("Where should not be null", where, notNullValue())
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 response.respond(false)
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onLocationChange(session: GeckoSession, url: String) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URL should not be null", url, notNullValue())
                 assertThat("URL should match", url, endsWith(HELLO_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("Cannot go back", canGoBack, equalTo(false))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("Cannot go forward", canGoForward, equalTo(false))
             }
 
             @AssertCalled(false)
             override fun onNewSession(session: GeckoSession, uri: String,
                                       response: GeckoResponse<GeckoSession>) {
@@ -194,38 +194,38 @@ class NavigationDelegateTest : BaseSessi
     @Test fun reload() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.session.reload()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
                                        where: Int,
                                        flags: Int,
                                        response: GeckoResponse<Boolean>) {
                 assertThat("URI should match", uri, endsWith(HELLO_HTML_PATH))
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 response.respond(false)
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onLocationChange(session: GeckoSession, url: String) {
                 assertThat("URL should match", url, endsWith(HELLO_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
                 assertThat("Cannot go back", canGoBack, equalTo(false))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
                 assertThat("Cannot go forward", canGoForward, equalTo(false))
             }
 
             @AssertCalled(false)
             override fun onNewSession(session: GeckoSession, uri: String,
                                       response: GeckoResponse<GeckoSession>) {
             }
@@ -245,74 +245,74 @@ class NavigationDelegateTest : BaseSessi
                 assertThat("URL should match", url, endsWith(HELLO2_HTML_PATH))
             }
         })
 
         sessionRule.session.goBack()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
                                        where: Int,
                                        flags: Int,
                                        response: GeckoResponse<Boolean>) {
                 assertThat("URI should match", uri, endsWith(HELLO_HTML_PATH))
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 response.respond(false)
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onLocationChange(session: GeckoSession, url: String) {
                 assertThat("URL should match", url, endsWith(HELLO_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
                 assertThat("Cannot go back", canGoBack, equalTo(false))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
                 assertThat("Can go forward", canGoForward, equalTo(true))
             }
 
             @AssertCalled(false)
             override fun onNewSession(session: GeckoSession, uri: String,
                                       response: GeckoResponse<GeckoSession>) {
             }
         })
 
         sessionRule.session.goForward()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.NavigationDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onLoadRequest(session: GeckoSession, uri: String,
                                        where: Int,
                                        flags: Int,
                                        response: GeckoResponse<Boolean>) {
                 assertThat("URI should match", uri, endsWith(HELLO2_HTML_PATH))
                 assertThat("Where should match", where,
                            equalTo(GeckoSession.NavigationDelegate.TARGET_WINDOW_CURRENT))
                 response.respond(false)
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onLocationChange(session: GeckoSession, url: String) {
                 assertThat("URL should match", url, endsWith(HELLO2_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
                 assertThat("Can go back", canGoBack, equalTo(true))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
                 assertThat("Cannot go forward", canGoForward, equalTo(false))
             }
 
             @AssertCalled(false)
             override fun onNewSession(session: GeckoSession, uri: String,
                                       response: GeckoResponse<GeckoSession>) {
             }
@@ -330,22 +330,22 @@ class NavigationDelegateTest : BaseSessi
             }
         })
 
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.session.loadTestPath(HELLO2_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 assertThat("URL should match", url, endsWith(HELLO2_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 assertThat("Load should succeed", success, equalTo(true))
             }
         })
     }
 
     @WithDisplay(width = 128, height = 128)
     @Ignore
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ProgressDelegateTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ProgressDelegateTest.kt
@@ -23,36 +23,36 @@ import org.junit.runner.RunWith
 @MediumTest
 class ProgressDelegateTest : BaseSessionTest() {
 
     @Test fun load() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("URL should not be null", url, notNullValue())
                 assertThat("URL should match", url, endsWith(HELLO_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onSecurityChange(session: GeckoSession,
                                           securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("Security info should not be null", securityInfo, notNullValue())
 
                 assertThat("Should not be secure", securityInfo.isSecure, equalTo(false))
                 assertThat("Tracking mode should match",
                            securityInfo.trackingMode,
                            equalTo(GeckoSession.ProgressDelegate.SecurityInformation.CONTENT_UNKNOWN))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(3))
+            @AssertCalled(count = 1, order = [3])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 assertThat("Session should not be null", session, notNullValue())
                 assertThat("Load should succeed", success, equalTo(true))
             }
         })
     }
 
     fun loadExpectNetError(testUri: String) {
@@ -90,23 +90,23 @@ class ProgressDelegateTest : BaseSession
 
     @Ignore
     @Test fun multipleLoads() {
         sessionRule.session.loadUri(INVALID_URI)
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStops(2)
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 2, order = intArrayOf(1, 3))
+            @AssertCalled(count = 2, order = [1, 3])
             override fun onPageStart(session: GeckoSession, url: String) {
                 assertThat("URL should match", url,
                            endsWith(forEachCall(INVALID_URI, HELLO_HTML_PATH)))
             }
 
-            @AssertCalled(count = 2, order = intArrayOf(2, 4))
+            @AssertCalled(count = 2, order = [2, 4])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 // The first load is certain to fail because of interruption by the second load
                 // or by invalid domain name, whereas the second load is certain to succeed.
                 assertThat("Success flag should match", success,
                            equalTo(forEachCall(false, true)))
             };
         })
     }
@@ -114,74 +114,74 @@ class ProgressDelegateTest : BaseSession
     @Test fun reload() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.session.reload()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 assertThat("URL should match", url, endsWith(HELLO_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onSecurityChange(session: GeckoSession,
                                           securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(3))
+            @AssertCalled(count = 1, order = [3])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 assertThat("Load should succeed", success, equalTo(true))
             }
         })
     }
 
     @Test fun goBackAndForward() {
         sessionRule.session.loadTestPath(HELLO_HTML_PATH)
         sessionRule.waitForPageStop()
         sessionRule.session.loadTestPath(HELLO2_HTML_PATH)
         sessionRule.waitForPageStop()
 
         sessionRule.session.goBack()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 assertThat("URL should match", url, endsWith(HELLO_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onSecurityChange(session: GeckoSession,
                                           securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(3))
+            @AssertCalled(count = 1, order = [3])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 assertThat("Load should succeed", success, equalTo(true))
             }
         })
 
         sessionRule.session.goForward()
         sessionRule.waitForPageStop()
 
         sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressDelegate {
-            @AssertCalled(count = 1, order = intArrayOf(1))
+            @AssertCalled(count = 1, order = [1])
             override fun onPageStart(session: GeckoSession, url: String) {
                 assertThat("URL should match", url, endsWith(HELLO2_HTML_PATH))
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(2))
+            @AssertCalled(count = 1, order = [2])
             override fun onSecurityChange(session: GeckoSession,
                                           securityInfo: GeckoSession.ProgressDelegate.SecurityInformation) {
             }
 
-            @AssertCalled(count = 1, order = intArrayOf(3))
+            @AssertCalled(count = 1, order = [3])
             override fun onPageStop(session: GeckoSession, success: Boolean) {
                 assertThat("Load should succeed", success, equalTo(true))
             }
         })
     }
 
     @Test fun correctSecurityInfoForValidTLS_automation() {
         assumeThat(sessionRule.env.isAutomation, equalTo(true))
--- a/mobile/android/modules/ActionBarHandler.jsm
+++ b/mobile/android/modules/ActionBarHandler.jsm
@@ -203,17 +203,17 @@ var ActionBarHandler = {
   _getSelectionTargets: function() {
     let [element, win] = [Services.focus.focusedElement, Services.focus.focusedWindow];
     if (!element) {
       // No focused editable.
       return [null, win];
     }
 
     // Return focused editable text element and its window.
-    if (((element instanceof Ci.nsIDOMHTMLInputElement) && element.mozIsTextField(false)) ||
+    if (((ChromeUtils.getClassName(element) === "HTMLInputElement") && element.mozIsTextField(false)) ||
         (ChromeUtils.getClassName(element) === "HTMLTextAreaElement") ||
         element.isContentEditable) {
       return [element, win];
     }
 
     // Focused element can't contain text.
     return [null, win];
   },
@@ -412,17 +412,18 @@ var ActionBarHandler = {
 
       selector: {
         matches: function(element, win) {
           // Can cut from editable, or design-mode document.
           if (!element && !ActionBarHandler._isInDesignMode(win)) {
             return false;
           }
           // Don't allow "cut" from password fields.
-          if (element instanceof Ci.nsIDOMHTMLInputElement &&
+          if (element &&
+              ChromeUtils.getClassName(element) === "HTMLInputElement" &&
               !element.mozIsTextField(true)) {
             return false;
           }
           // Don't allow "cut" from disabled/readonly fields.
           if (element && (element.disabled || element.readOnly)) {
             return false;
           }
           // Allow if selected text exists.
@@ -453,17 +454,18 @@ var ActionBarHandler = {
       label: () => Strings.browser.GetStringFromName("contextmenu.copy"),
       icon: "drawable://ab_copy",
       order: 3,
       floatingOrder: 2,
 
       selector: {
         matches: function(element, win) {
           // Don't allow "copy" from password fields.
-          if (element instanceof Ci.nsIDOMHTMLInputElement &&
+          if (element &&
+              ChromeUtils.getClassName(element) === "HTMLInputElement" &&
               !element.mozIsTextField(true)) {
             return false;
           }
           // Allow if selected text exists.
           return (ActionBarHandler._getSelectedText().length > 0);
         },
       },
 
@@ -601,17 +603,17 @@ var ActionBarHandler = {
       floatingOrder: 8,
 
       selector: {
         matches: function(element, win) {
           let chrome = GeckoViewUtils.getChromeWindow(win);
           if (!chrome.SearchEngines) {
             return false;
           }
-          if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
+          if (!element || ChromeUtils.getClassName(element) !== "HTMLInputElement") {
             return false;
           }
           let form = element.form;
           if (!form || element.type == "password") {
             return false;
           }
 
           let method = form.method.toUpperCase();
--- a/mobile/android/modules/FormAssistant.jsm
+++ b/mobile/android/modules/FormAssistant.jsm
@@ -193,17 +193,17 @@ var FormAssistant = {
         }
         break;
       }
     }
   },
 
   // We only want to show autocomplete suggestions for certain elements
   _isAutoComplete: function(aElement) {
-    return (aElement instanceof Ci.nsIDOMHTMLInputElement) &&
+    return (ChromeUtils.getClassName(aElement) === "HTMLInputElement") &&
            !aElement.readOnly &&
            !this._isDisabledElement(aElement) &&
            (aElement.type !== "password") &&
            (aElement.autocomplete !== "off");
   },
 
   // Retrieves autocomplete suggestions for an element from the form autocomplete service.
   // aCallback(array_of_suggestions) is called when results are available.
@@ -235,17 +235,17 @@ var FormAssistant = {
   },
 
   /**
    * This function is similar to getListSuggestions from
    * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
    * used by the autocomplete.xml binding which is not in used in fennec
    */
   _getListSuggestions: function(aElement) {
-    if (!(aElement instanceof Ci.nsIDOMHTMLInputElement) || !aElement.list) {
+    if (ChromeUtils.getClassName(aElement) !== "HTMLInputElement" || !aElement.list) {
       return [];
     }
 
     let suggestions = [];
     let filter = !aElement.hasAttribute("mozNoFilter");
     let lowerFieldValue = aElement.value.toLowerCase();
 
     let options = aElement.list.options;
--- a/mobile/android/tests/browser/robocop/testAccessibleCarets.js
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.js
@@ -43,19 +43,18 @@ function do_promiseTabChangeEvent(tabId,
 
 /**
  * Selection methods vary if we have an input / textarea element,
  * or if we have basic content.
  */
 function isInputOrTextarea(element) {
   // ChromeUtils isn't included in robocop tests, so we have to use a different
   // way to test elements.
-  return ((element instanceof Ci.nsIDOMHTMLInputElement) ||
-          (element.localName === "textarea" &&
-           element.namespaceURI === "http://www.w3.org/1999/xhtml"));
+  return (element.namespaceURI === "http://www.w3.org/1999/xhtml" &&
+         (element.localName === "input" || element.localName === "textarea"));
 }
 
 /**
  * Return the selection controller based on element.
  */
 function elementSelection(element) {
   return (isInputOrTextarea(element)) ?
     element.editor.selection :
--- a/mobile/locales/searchplugins/wikipedia-NN.xml
+++ b/mobile/locales/searchplugins/wikipedia-NN.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://nn.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://nn.wikipedia.org/wiki/Spesial:Søk">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://nn.m.wikipedia.org/wiki/Spesial:Søk">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://nn.wikipedia.org/wiki/Spesial:Søk</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-NO.xml
+++ b/mobile/locales/searchplugins/wikipedia-NO.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://no.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://no.wikipedia.org/wiki/Spesial:Søk">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://no.m.wikipedia.org/wiki/Spesial:Søk">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://no.wikipedia.org/wiki/Spesial:Søk</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-an.xml
+++ b/mobile/locales/searchplugins/wikipedia-an.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://an.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://an.wikipedia.org/wiki/Especial:Mirar">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://an.m.wikipedia.org/wiki/Especial:Mirar">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://an.wikipedia.org/wiki/Especial:Mirar</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ar.xml
+++ b/mobile/locales/searchplugins/wikipedia-ar.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ar.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ar.wikipedia.org/wiki/خاص:بحث">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ar.m.wikipedia.org/wiki/خاص:بحث">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ar.wikipedia.org/wiki/خاص:بحث</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-as.xml
+++ b/mobile/locales/searchplugins/wikipedia-as.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://as.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://as.wikipedia.org/wiki/বিশেষ:সন্ধান">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://as.m.wikipedia.org/wiki/বিশেষ:সন্ধান">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://as.wikipedia.org/wiki/বিশেষ:সন্ধান</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ast.xml
+++ b/mobile/locales/searchplugins/wikipedia-ast.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ast.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ast.wikipedia.org/wiki/Especial:Gueta">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ast.m.wikipedia.org/wiki/Especial:Gueta">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ast.wikipedia.org/wiki/Especial:Gueta</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-az.xml
+++ b/mobile/locales/searchplugins/wikipedia-az.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://az.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://az.wikipedia.org/wiki/Xüsusi:Axtar">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://az.m.wikipedia.org/wiki/Xüsusi:Axtar">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://az.wikipedia.org/wiki/Xüsusi:Axtar</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-be.xml
+++ b/mobile/locales/searchplugins/wikipedia-be.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://be.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://be.wikipedia.org/wiki/Адмысловае:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://be.m.wikipedia.org/wiki/Адмысловае:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://be.wikipedia.org/wiki/Адмысловае:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-bg.xml
+++ b/mobile/locales/searchplugins/wikipedia-bg.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://bg.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://bg.wikipedia.org/wiki/Специални:Търсене">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://bg.wikipedia.org/wiki/Специални:Търсене">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://bg.wikipedia.org/wiki/Специални:Търсене</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-bn.xml
+++ b/mobile/locales/searchplugins/wikipedia-bn.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://bn.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://bn.wikipedia.org/wiki/বিশেষ:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://bn.m.wikipedia.org/wiki/বিশেষ:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://bn.wikipedia.org/wiki/বিশেষ:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-br.xml
+++ b/mobile/locales/searchplugins/wikipedia-br.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://br.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://br.wikipedia.org/wiki/Dibar:Klask">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://br.m.wikipedia.org/wiki/Dibar:Klask">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://br.wikipedia.org/wiki/Dibar:Klask</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-bs.xml
+++ b/mobile/locales/searchplugins/wikipedia-bs.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://bs.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://bs.wikipedia.org/wiki/Posebno:Pretraga">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://bs.wikipedia.org/wiki/Posebno:Pretraga">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://bs.wikipedia.org/wiki/Posebno:Pretraga</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ca.xml
+++ b/mobile/locales/searchplugins/wikipedia-ca.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ca.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ca.wikipedia.org/wiki/Especial:Cerca">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ca.m.wikipedia.org/wiki/Especial:Cerca">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ca.wikipedia.org/wiki/Especial:Cerca</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-cy.xml
+++ b/mobile/locales/searchplugins/wikipedia-cy.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://cy.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://cy.wikipedia.org/wiki/Arbennig:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://cy.m.wikipedia.org/wiki/Arbennig:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://cy.wikipedia.org/wiki/Arbennig:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-cz.xml
+++ b/mobile/locales/searchplugins/wikipedia-cz.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://cs.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://cs.wikipedia.org/wiki/Speciální:Hledání">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://cs.m.wikipedia.org/wiki/Speciální:Hledání">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://cs.wikipedia.org/wiki/Speciální:Hledání</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-da.xml
+++ b/mobile/locales/searchplugins/wikipedia-da.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://da.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://da.wikipedia.org/wiki/Speciel:Søgning">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://da.m.wikipedia.org/wiki/Speciel:Søgning">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://da.wikipedia.org/wiki/Speciel:Søgning</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-de.xml
+++ b/mobile/locales/searchplugins/wikipedia-de.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://de.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://de.wikipedia.org/wiki/Spezial:Suche">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://de.m.wikipedia.org/wiki/Spezial:Suche">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://de.wikipedia.org/wiki/Spezial:Suche</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-dsb.xml
+++ b/mobile/locales/searchplugins/wikipedia-dsb.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://dsb.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://dsb.wikipedia.org/wiki/Specialne:Pytaś">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://dsb.m.wikipedia.org/wiki/Specialne:Pytaś">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://dsb.wikipedia.org/wiki/Specialne:Pytaś</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-el.xml
+++ b/mobile/locales/searchplugins/wikipedia-el.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://el.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://el.wikipedia.org/wiki/Ειδικό:Αναζήτηση">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://el.m.wikipedia.org/wiki/Ειδικό:Αναζήτηση">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://el.wikipedia.org/wiki/Ειδικό:Αναζήτηση</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-eo.xml
+++ b/mobile/locales/searchplugins/wikipedia-eo.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://eo.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://eo.wikipedia.org/wiki/Specialaĵo:Serĉi">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://eo.m.wikipedia.org/wiki/Specialaĵo:Serĉi">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://eo.wikipedia.org/wiki/Specialaĵo:Serĉi</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-es.xml
+++ b/mobile/locales/searchplugins/wikipedia-es.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://es.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://es.wikipedia.org/wiki/Especial:Buscar">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://es.m.wikipedia.org/wiki/Especial:Buscar">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://es.wikipedia.org/wiki/Especial:Buscar</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-et.xml
+++ b/mobile/locales/searchplugins/wikipedia-et.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://et.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://et.wikipedia.org/wiki/Eri:Otsimine">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://et.m.wikipedia.org/wiki/Eri:Otsimine">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://et.wikipedia.org/wiki/Eri:Otsimine</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-eu.xml
+++ b/mobile/locales/searchplugins/wikipedia-eu.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://eu.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://eu.wikipedia.org/wiki/Berezi:Bilatu">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://eu.m.wikipedia.org/wiki/Special:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://eu.wikipedia.org/wiki/Berezi:Bilatu</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-fa.xml
+++ b/mobile/locales/searchplugins/wikipedia-fa.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://fa.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://fa.wikipedia.org/wiki/ویژه:جستجو">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://fa.m.wikipedia.org/wiki/ویژه:جستجو">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://fa.wikipedia.org/wiki/ویژه:جستجو</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-fi.xml
+++ b/mobile/locales/searchplugins/wikipedia-fi.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://fi.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://fi.wikipedia.org/wiki/Toiminnot:Haku">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://fi.m.wikipedia.org/wiki/Toiminnot:Haku">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://fi.wikipedia.org/wiki/Toiminnot:Haku</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-fr.xml
+++ b/mobile/locales/searchplugins/wikipedia-fr.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://fr.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://fr.wikipedia.org/wiki/Spécial:Recherche">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://fr.m.wikipedia.org/wiki/Spécial:Recherche">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://fr.wikipedia.org/wiki/Spécial:Recherche</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-fy-NL.xml
+++ b/mobile/locales/searchplugins/wikipedia-fy-NL.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://fy.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://fy.wikipedia.org/wiki/Wiki:Sykje">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://fy.m.wikipedia.org/wiki/Wiki:Sykje">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://fy.wikipedia.org/wiki/Wiki:Sykje</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ga-IE.xml
+++ b/mobile/locales/searchplugins/wikipedia-ga-IE.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ga.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ga.wikipedia.org/wiki/Speisialta:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ga.m.wikipedia.org/wiki/Speisialta:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ga.wikipedia.org/wiki/Speisialta:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-gd.xml
+++ b/mobile/locales/searchplugins/wikipedia-gd.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://gd.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://gd.wikipedia.org/wiki/Sònraichte:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://gd.m.wikipedia.org/wiki/Sònraichte:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://gd.wikipedia.org/wiki/Sònraichte:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-gl.xml
+++ b/mobile/locales/searchplugins/wikipedia-gl.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://gl.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://gl.wikipedia.org/wiki/Especial:Procurar">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://gl.m.wikipedia.org/wiki/Especial:Procurar">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://gl.wikipedia.org/wiki/Especial:Procurar</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-gn.xml
+++ b/mobile/locales/searchplugins/wikipedia-gn.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://gn.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://gn.wikipedia.org/wiki/Mba'echĩchĩ:Buscar">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://gn.m.wikipedia.org/wiki/Mba'echĩchĩ:Buscar">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://gn.wikipedia.org/wiki/Mba'echĩchĩ:Buscar</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-gu.xml
+++ b/mobile/locales/searchplugins/wikipedia-gu.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://gu.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://gu.wikipedia.org/wiki/વિશેષ:શોધ">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://gu.m.wikipedia.org/wiki/વિશેષ:શોધ">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://gu.wikipedia.org/wiki/વિશેષ:શોધ</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-he.xml
+++ b/mobile/locales/searchplugins/wikipedia-he.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://he.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://he.wikipedia.org/wiki/מיוחד:חיפוש">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://he.m.wikipedia.org/wiki/מיוחד:חיפוש">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://he.wikipedia.org/wiki/מיוחד:חיפוש</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-hi.xml
+++ b/mobile/locales/searchplugins/wikipedia-hi.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://hi.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://hi.wikipedia.org/wiki/विशेष:खोज">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://hi.m.wikipedia.org/wiki/विशेष:खोज">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://hi.wikipedia.org/wiki/विशेष:खोज</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-hr.xml
+++ b/mobile/locales/searchplugins/wikipedia-hr.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://hr.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://hr.wikipedia.org/wiki/Posebno:Traži">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://hr.m.wikipedia.org/wiki/Posebno:Traži">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://hr.wikipedia.org/wiki/Posebno:Traži</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-hsb.xml
+++ b/mobile/locales/searchplugins/wikipedia-hsb.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://hsb.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://hsb.wikipedia.org/wiki/Specialnje:Pytać">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://hsb.m.wikipedia.org/wiki/Specialnje:Pytać">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://hsb.wikipedia.org/wiki/Specialnje:Pytać</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-hu.xml
+++ b/mobile/locales/searchplugins/wikipedia-hu.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://hu.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://hu.wikipedia.org/wiki/Speciális:Keresés">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://hu.m.wikipedia.org/wiki/Speciális:Keresés">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://hu.wikipedia.org/wiki/Speciális:Keresés</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-hy-AM.xml
+++ b/mobile/locales/searchplugins/wikipedia-hy-AM.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://hy.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://hy.wikipedia.org/wiki/Սպասարկող:Որոնել">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://hy.m.wikipedia.org/wiki/Սպասարկող:Որոնել">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://hy.wikipedia.org/wiki/Սպասարկող:Որոնել</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ia.xml
+++ b/mobile/locales/searchplugins/wikipedia-ia.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ia.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ia.wikipedia.org/wiki/Special:Recerca">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ia.wikipedia.org/wiki/Special:Recerca">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ia.wikipedia.org/wiki/Special:Recerca</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-id.xml
+++ b/mobile/locales/searchplugins/wikipedia-id.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://id.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://id.wikipedia.org/wiki/Istimewa:Pencarian">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://id.m.wikipedia.org/wiki/Istimewa:Pencarian">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://id.wikipedia.org/wiki/Istimewa:Pencarian</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-is.xml
+++ b/mobile/locales/searchplugins/wikipedia-is.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://is.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://is.wikipedia.org/wiki/Kerfissíða:Leit">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://is.m.wikipedia.org/wiki/Kerfissíða:Leit">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://is.wikipedia.org/wiki/Kerfissíða:Leit</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-it.xml
+++ b/mobile/locales/searchplugins/wikipedia-it.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://it.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://it.wikipedia.org/wiki/Speciale:Ricerca">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://it.m.wikipedia.org/wiki/Speciale:Ricerca">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://it.wikipedia.org/wiki/Speciale:Ricerca</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ja.xml
+++ b/mobile/locales/searchplugins/wikipedia-ja.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ja.wikipedia.org/wiki">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ja.wikipedia.org/wiki/特別:検索">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ja.m.wikipedia.org/wiki/特別:検索">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ja.wikipedia.org/wiki/特別:検索</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ka.xml
+++ b/mobile/locales/searchplugins/wikipedia-ka.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ka.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ka.wikipedia.org/wiki/სპეციალური:ძიება">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ka.m.wikipedia.org/wiki/სპეციალური:ძიება">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ka.wikipedia.org/wiki/სპეციალური:ძიება</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-kab.xml
+++ b/mobile/locales/searchplugins/wikipedia-kab.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://kab.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://kab.wikipedia.org/wiki/Uslig:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://kab.wikipedia.org/wiki/Uslig:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://kab.wikipedia.org/wiki/Uslig:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-kk.xml
+++ b/mobile/locales/searchplugins/wikipedia-kk.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://kk.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://kk.wikipedia.org/wiki/Арнайы:Іздеу">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://kk.m.wikipedia.org/wiki/Арнайы:Іздеу">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://kk.wikipedia.org/wiki/Арнайы:Іздеу</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-km.xml
+++ b/mobile/locales/searchplugins/wikipedia-km.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://km.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://km.wikipedia.org/wiki/ពិសេស:ស្វែងរក">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://km.m.wikipedia.org/wiki/ពិសេស:ស្វែងរក">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://km.wikipedia.org/wiki/ពិសេស:ស្វែងរក</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-kn.xml
+++ b/mobile/locales/searchplugins/wikipedia-kn.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://kn.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://kn.wikipedia.org/wiki/ವಿಶೇಷ:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://kn.m.wikipedia.org/wiki/ವಿಶೇಷ:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://kn.wikipedia.org/wiki/ವಿಶೇಷ:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-lij.xml
+++ b/mobile/locales/searchplugins/wikipedia-lij.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://lij.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://lij.wikipedia.org/wiki/Speçiale:Riçerca">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://lij.m.wikipedia.org/wiki/Speçiale:Riçerca">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://lij.wikipedia.org/wiki/Speçiale:Riçerca</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-lo.xml
+++ b/mobile/locales/searchplugins/wikipedia-lo.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://lo.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://lo.wikipedia.org/wiki/ພິເສດ:ຊອກຫາ">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://lo.m.wikipedia.org/wiki/ພິເສດ:ຊອກຫາ">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://lo.wikipedia.org/wiki/ພິເສດ:ຊອກຫາ</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-lt.xml
+++ b/mobile/locales/searchplugins/wikipedia-lt.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://lt.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://lt.wikipedia.org/wiki/Specialus:Paieška">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://lt.m.wikipedia.org/wiki/Specialus:Paieška">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://lt.wikipedia.org/wiki/Specialus:Paieška</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ltg.xml
+++ b/mobile/locales/searchplugins/wikipedia-ltg.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ltg.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ltg.wikipedia.org/wiki/Seviškuo:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ltg.wikipedia.org/wiki/Seviškuo:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ltg.wikipedia.org/wiki/Seviškuo:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-lv.xml
+++ b/mobile/locales/searchplugins/wikipedia-lv.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://lv.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://lv.wikipedia.org/wiki/Special:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://lv.m.wikipedia.org/wiki/Special:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://lv.wikipedia.org/wiki/Special:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ml.xml
+++ b/mobile/locales/searchplugins/wikipedia-ml.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ml.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ml.wikipedia.org/wiki/പ്രത്യേകം:അന്വേഷണം">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ml.m.wikipedia.org/wiki/പ്രത്യേകം:അന്വേഷണം">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ml.wikipedia.org/wiki/പ്രത്യേകം:അന്വേഷണം</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-mr.xml
+++ b/mobile/locales/searchplugins/wikipedia-mr.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://mr.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://mr.wikipedia.org/wiki/विशेष:शोधा">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://mr.m.wikipedia.org/wiki/विशेष:शोधा">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://mr.wikipedia.org/wiki/विशेष:शोधा</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ms.xml
+++ b/mobile/locales/searchplugins/wikipedia-ms.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ms.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ms.wikipedia.org/wiki/Khas:Gelintar">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ms.m.wikipedia.org/wiki/Khas:Gelintar">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ms.wikipedia.org/wiki/Khas:Gelintar</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-my.xml
+++ b/mobile/locales/searchplugins/wikipedia-my.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://my.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://my.wikipedia.org/wiki/Special:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://my.m.wikipedia.org/wiki/Special:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://my.wikipedia.org/wiki/Special:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ne.xml
+++ b/mobile/locales/searchplugins/wikipedia-ne.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ne.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ne.wikipedia.org/wiki/विशेष:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ne.wikipedia.org/wiki/विशेष:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ne.wikipedia.org/wiki/विशेष:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-nl.xml
+++ b/mobile/locales/searchplugins/wikipedia-nl.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://nl.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://nl.wikipedia.org/wiki/Speciaal:Zoeken">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://nl.m.wikipedia.org/wiki/Speciaal:Zoeken">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://nl.wikipedia.org/wiki/Speciaal:Zoeken</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-oc.xml
+++ b/mobile/locales/searchplugins/wikipedia-oc.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://oc.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://oc.wikipedia.org/wiki/Especial:Recèrca">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://oc.wikipedia.org/wiki/Especial:Recèrca">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://oc.wikipedia.org/wiki/Especial:Recèrca</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-or.xml
+++ b/mobile/locales/searchplugins/wikipedia-or.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://or.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://or.wikipedia.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://or.m.wikipedia.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://or.wikipedia.org/wiki/ବିଶେଷ:ଖୋଜନ୍ତୁ</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-pa.xml
+++ b/mobile/locales/searchplugins/wikipedia-pa.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://pa.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://pa.wikipedia.org/wiki/ਖ਼ਾਸ:ਖੋਜੋ">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://pa.m.wikipedia.org/wiki/ਖ਼ਾਸ:ਖੋਜੋ">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://pa.wikipedia.org/wiki/ਖ਼ਾਸ:ਖੋਜੋ</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-pl.xml
+++ b/mobile/locales/searchplugins/wikipedia-pl.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://pl.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://pl.wikipedia.org/wiki/Specjalna:Szukaj">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://pl.m.wikipedia.org/wiki/Specjalna:Szukaj">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://pl.wikipedia.org/wiki/Specjalna:Szukaj</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-pt.xml
+++ b/mobile/locales/searchplugins/wikipedia-pt.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://pt.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://pt.wikipedia.org/wiki/Especial:Pesquisar">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://pt.m.wikipedia.org/wiki/Especial:Pesquisar">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://pt.wikipedia.org/wiki/Especial:Pesquisar</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-rm.xml
+++ b/mobile/locales/searchplugins/wikipedia-rm.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://rm.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://rm.wikipedia.org/wiki/Spezial:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://rm.m.wikipedia.org/wiki/Spezial:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://rm.wikipedia.org/wiki/Spezial:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ro.xml
+++ b/mobile/locales/searchplugins/wikipedia-ro.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ro.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ro.wikipedia.org/wiki/Special:Căutare">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ro.m.wikipedia.org/wiki/Special:Căutare">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ro.wikipedia.org/wiki/Special:Căutare</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ru.xml
+++ b/mobile/locales/searchplugins/wikipedia-ru.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ru.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ru.wikipedia.org/wiki/Служебная:Поиск">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ru.m.wikipedia.org/wiki/Служебная:Поиск">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ru.wikipedia.org/wiki/Служебная:Поиск</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-sk.xml
+++ b/mobile/locales/searchplugins/wikipedia-sk.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://sk.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://sk.wikipedia.org/wiki/Špeciálne:Hľadanie">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://sk.m.wikipedia.org/wiki/Špeciálne:Hľadanie">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://sk.wikipedia.org/wiki/Špeciálne:Hľadanie</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-sl.xml
+++ b/mobile/locales/searchplugins/wikipedia-sl.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://sl.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://sl.wikipedia.org/wiki/Posebno:Iskanje">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://sl.m.wikipedia.org/wiki/Posebno:Iskanje">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://sl.wikipedia.org/wiki/Posebno:Iskanje</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-sq.xml
+++ b/mobile/locales/searchplugins/wikipedia-sq.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://sq.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://sq.wikipedia.org/wiki/Speciale:Kërkim">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://sq.m.wikipedia.org/wiki/Speciale:Kërkim">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://sq.wikipedia.org/wiki/Speciale:Kërkim</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-sr.xml
+++ b/mobile/locales/searchplugins/wikipedia-sr.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://sr.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://sr.wikipedia.org/wiki/Посебно:Претражи">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://sr.m.wikipedia.org/wiki/Посебно:Претражи">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://sr.wikipedia.org/wiki/Посебно:Претражи</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-sv-SE.xml
+++ b/mobile/locales/searchplugins/wikipedia-sv-SE.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://sv.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://sv.wikipedia.org/wiki/Special:Sök">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://sv.m.wikipedia.org/wiki/Special:Sök">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://sv.wikipedia.org/wiki/Special:Sök</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ta.xml
+++ b/mobile/locales/searchplugins/wikipedia-ta.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ta.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ta.wikipedia.org/wiki/சிறப்பு:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ta.m.wikipedia.org/wiki/சிறப்பு:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ta.wikipedia.org/wiki/சிறப்பு:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-te.xml
+++ b/mobile/locales/searchplugins/wikipedia-te.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://te.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://te.wikipedia.org/wiki/ప్రత్యేక:అన్వేషణ">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://te.m.wikipedia.org/wiki/ప్రత్యేక:అన్వేషణ">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://te.wikipedia.org/wiki/ప్రత్యేక:అన్వేషణ</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-th.xml
+++ b/mobile/locales/searchplugins/wikipedia-th.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://th.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://th.wikipedia.org/wiki/พิเศษ:ค้นหา">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://th.m.wikipedia.org/wiki/พิเศษ:ค้นหา">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://th.wikipedia.org/wiki/พิเศษ:ค้นหา</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-tr.xml
+++ b/mobile/locales/searchplugins/wikipedia-tr.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://tr.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://tr.wikipedia.org/wiki/Özel:Ara">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://tr.m.wikipedia.org/wiki/Özel:Ara">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://tr.wikipedia.org/wiki/Özel:Ara</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-uk.xml
+++ b/mobile/locales/searchplugins/wikipedia-uk.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://uk.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://uk.wikipedia.org/wiki/Спеціальна:Пошук">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://uk.m.wikipedia.org/wiki/Спеціальна:Пошук">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://uk.wikipedia.org/wiki/Спеціальна:Пошук</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-ur.xml
+++ b/mobile/locales/searchplugins/wikipedia-ur.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://ur.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://ur.wikipedia.org/wiki/خاص:تلاش">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://ur.wikipedia.org/wiki/خاص:تلاش">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://ur.wikipedia.org/wiki/خاص:تلاش</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-uz.xml
+++ b/mobile/locales/searchplugins/wikipedia-uz.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://uz.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://uz.wikipedia.org/wiki/Maxsus:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://uz.wikipedia.org/wiki/Maxsus:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://uz.wikipedia.org/wiki/Maxsus:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-vi.xml
+++ b/mobile/locales/searchplugins/wikipedia-vi.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="get" template="https://vi.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="get" template="https://vi.wikipedia.org/wiki/Đặc_biệt:Tìm_kiếm">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://vi.m.wikipedia.org/wiki/Đặc_biệt:Tìm_kiếm">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://vi.wikipedia.org/wiki/Đặc_biệt:Tìm_kiếm</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-wo.xml
+++ b/mobile/locales/searchplugins/wikipedia-wo.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://wo.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://wo.wikipedia.org/wiki/Jagleel:Ceet">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://wo.wikipedia.org/wiki/Jagleel:Ceet">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://wo.wikipedia.org/wiki/Jagleel:Ceet</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-zh-CN.xml
+++ b/mobile/locales/searchplugins/wikipedia-zh-CN.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://zh.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://zh.wikipedia.org/wiki/Special:搜索">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://zh.m.wikipedia.org/wiki/Special:搜索">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://zh.wikipedia.org/wiki/Special:搜索</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia-zh-TW.xml
+++ b/mobile/locales/searchplugins/wikipedia-zh-TW.xml
@@ -10,16 +10,11 @@
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://zh.wikipedia.org/wiki/Special:搜索">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
   <Param name="variant" value="zh-tw"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://zh.m.wikipedia.org/wiki/Special:搜索">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-  <Param name="variant" value="zh-tw"/>
 </Url>
 <SearchForm>https://zh.wikipedia.org/wiki/Special:搜索</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/searchplugins/wikipedia.xml
+++ b/mobile/locales/searchplugins/wikipedia.xml
@@ -9,15 +9,10 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://en.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://en.wikipedia.org/wiki/Special:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
-<!-- Search activity -->
-<Url type="text/html" method="GET" rel="mobile" template="https://en.m.wikipedia.org/wiki/Special:Search">
-  <Param name="search" value="{searchTerms}"/>
-  <Param name="sourceid" value="Mozilla-search"/>
-</Url>
 <SearchForm>https://en.wikipedia.org/wiki/Special:Search</SearchForm>
 </SearchPlugin>
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -893,20 +893,24 @@ class RunProgram(MachCommandBase):
                    'browser.shell.checkDefaultBrowser': False,
                    'general.warnOnAboutConfig': False,
                 }
                 prefs.update(self._mach_context.settings.runprefs)
                 prefs.update([p.split('=', 1) for p in setpref])
                 for pref in prefs:
                     prefs[pref] = Preferences.cast(prefs[pref])
 
+                tmpdir = os.path.join(self.topobjdir, 'tmp')
+                if not os.path.exists(tmpdir):
+                    os.makedirs(tmpdir)
+
                 if (temp_profile):
-                    path = tempfile.mkdtemp(dir=os.path.join(self.topobjdir, 'tmp'), prefix='profile-')
+                    path = tempfile.mkdtemp(dir=tmpdir, prefix='profile-')
                 else:
-                    path = os.path.join(self.topobjdir, 'tmp', 'profile-default')
+                    path = os.path.join(tmpdir, 'profile-default')
 
                 profile = Profile(path, preferences=prefs)
                 args.append('-profile')
                 args.append(profile.profile)
 
             if not no_profile_option_given and setpref:
                 print("setpref is only supported if a profile is not specified")
                 return 1
--- a/services/common/blocklist-clients.js
+++ b/services/common/blocklist-clients.js
@@ -33,17 +33,17 @@ const PREF_BLOCKLIST_GFX_COLLECTION     
 const PREF_BLOCKLIST_GFX_CHECKED_SECONDS     = "services.blocklist.gfx.checked";
 const PREF_BLOCKLIST_GFX_SIGNER              = "services.blocklist.gfx.signer";
 
 /**
  * Revoke the appropriate certificates based on the records from the blocklist.
  *
  * @param {Object} data   Current records in the local db.
  */
-async function updateCertBlocklist({data: records}) {
+async function updateCertBlocklist({ data: { current: records } }) {
   const certList = Cc["@mozilla.org/security/certblocklist;1"]
                      .getService(Ci.nsICertBlocklist);
   for (let item of records) {
     try {
       if (item.issuerName && item.serialNumber) {
         certList.revokeCertByIssuerAndSerial(item.issuerName,
                                             item.serialNumber);
       } else if (item.subject && item.pubKeyHash) {
@@ -61,17 +61,17 @@ async function updateCertBlocklist({data
 }
 
 /**
  * Modify the appropriate security pins based on records from the remote
  * collection.
  *
  * @param {Object} data   Current records in the local db.
  */
-async function updatePinningList({data: records}) {
+async function updatePinningList({ data: { current: records } }) {
   if (!Services.prefs.getBoolPref(PREF_BLOCKLIST_PINNING_ENABLED)) {
     return;
   }
 
   const siteSecurityService = Cc["@mozilla.org/ssservice;1"]
       .getService(Ci.nsISiteSecurityService);
 
   // clear the current preload list
@@ -104,17 +104,17 @@ async function updatePinningList({data: 
 }
 
 /**
  * Write list of records into JSON file, and notify nsBlocklistService.
  *
  * @param {Object} client   RemoteSettingsClient instance
  * @param {Object} data      Current records in the local db.
  */
-async function updateJSONBlocklist(client, { data: records }) {
+async function updateJSONBlocklist(client, { data: { current: records } }) {
   // Write JSON dump for synchronous load at startup.
   const path = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
   const blocklistFolder = OS.Path.dirname(path);
 
   await OS.File.makeDir(blocklistFolder, {from: OS.Constants.Path.profileDir});
 
   const serialized = JSON.stringify({data: records}, null, 2);
   try {
@@ -134,38 +134,38 @@ var PinningBlocklistClient;
 var PluginBlocklistClient;
 
 function initialize() {
   OneCRLBlocklistClient = RemoteSettings(Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_COLLECTION), {
     bucketName: Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET),
     lastCheckTimePref: PREF_BLOCKLIST_ONECRL_CHECKED_SECONDS,
     signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_ONECRL_SIGNER),
   });
-  OneCRLBlocklistClient.on("change", updateCertBlocklist);
+  OneCRLBlocklistClient.on("sync", updateCertBlocklist);
 
   AddonBlocklistClient = RemoteSettings(Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION), {
     bucketName: Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET),
     lastCheckTimePref: PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS,
     signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_SIGNER),
   });
-  AddonBlocklistClient.on("change", updateJSONBlocklist.bind(null, AddonBlocklistClient));
+  AddonBlocklistClient.on("sync", updateJSONBlocklist.bind(null, AddonBlocklistClient));
 
   PluginBlocklistClient = RemoteSettings(Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION), {
     bucketName: Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET),
     lastCheckTimePref: PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS,
     signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_SIGNER),
   });
-  PluginBlocklistClient.on("change", updateJSONBlocklist.bind(null, PluginBlocklistClient));
+  PluginBlocklistClient.on("sync", updateJSONBlocklist.bind(null, PluginBlocklistClient));
 
   GfxBlocklistClient = RemoteSettings(Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION), {
     bucketName: Services.prefs.getCharPref(PREF_BLOCKLIST_BUCKET),
     lastCheckTimePref: PREF_BLOCKLIST_GFX_CHECKED_SECONDS,
     signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_SIGNER),
   });
-  GfxBlocklistClient.on("change", updateJSONBlocklist.bind(null, GfxBlocklistClient));
+  GfxBlocklistClient.on("sync", updateJSONBlocklist.bind(null, GfxBlocklistClient));
 
   PinningBlocklistClient = RemoteSettings(Services.prefs.getCharPref(PREF_BLOCKLIST_PINNING_COLLECTION), {
     bucketName: Services.prefs.getCharPref(PREF_BLOCKLIST_PINNING_BUCKET),
     lastCheckTimePref: PREF_BLOCKLIST_PINNING_CHECKED_SECONDS,
     signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_PINNING_SIGNER),
   });
-  PinningBlocklistClient.on("change", updatePinningList);
+  PinningBlocklistClient.on("sync", updatePinningList);
 }
--- a/services/common/docs/RemoteSettings.rst
+++ b/services/common/docs/RemoteSettings.rst
@@ -46,23 +46,28 @@ The list can optionally be filtered or o
         "enabled": true,
       },
       order: "-weight"
     });
 
 Events
 ------
 
-The ``change`` event allows to be notified when the remote settings are changed. The event ``data`` attribute contains the whole new list of settings.
+The ``on()`` function registers handlers to be triggered when events occur.
+
+The ``sync`` event allows to be notified when the remote settings are changed on the server side. Your handler is given an ``event`` object that contains a ``data`` attribute that has information about the changes:
+
+- ``current``: current list of entries (after changes were applied);
+- ``created``, ``updated``, ``deleted``: list of entries that were created/updated/deleted respectively.
 
 .. code-block:: js
 
-    RemoteSettings("a-key").on("change", event => {
-      const { data } = event;
-      for(const entry of data) {
+    RemoteSettings("a-key").on("sync", event => {
+      const { data: { current } } = event;
+      for(const entry of current) {
         // Do something with entry...
         // await InternalAPI.reload(entry.id, entry.label, entry.weight);
       }
     });
 
 .. note::
     Currently, the update of remote settings is triggered by the `nsBlocklistService <https://dxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/nsBlocklistService.js>`_ (~ every 24H).
 
--- a/services/common/remote-settings.js
+++ b/services/common/remote-settings.js
@@ -126,17 +126,17 @@ class RemoteSettingsClient {
 
   constructor(collectionName, { lastCheckTimePref, bucketName, signerName }) {
     this.collectionName = collectionName;
     this.lastCheckTimePref = lastCheckTimePref;
     this.bucketName = bucketName;
     this.signerName = signerName;
 
     this._callbacks = new Map();
-    this._callbacks.set("change", []);
+    this._callbacks.set("sync", []);
 
     this._kinto = null;
   }
 
   get identifier() {
     return `${this.bucketName}/${this.collectionName}`;
   }
 
@@ -237,20 +237,22 @@ class RemoteSettingsClient {
       // to record the fact that a check happened.
       if (lastModified <= collectionLastModified) {
         this._updateLastCheck(serverTime);
         reportStatus = UptakeTelemetry.STATUS.UP_TO_DATE;
         return;
       }
 
       // Fetch changes from server.
+      let syncResult;
       try {
         // Server changes have priority during synchronization.
         const strategy = Kinto.syncStrategy.SERVER_WINS;
-        const { ok } = await collection.sync({remote, strategy});
+        syncResult = await collection.sync({remote, strategy});
+        const { ok } = syncResult;
         if (!ok) {
           // Some synchronization conflicts occured.
           reportStatus = UptakeTelemetry.STATUS.CONFLICT_ERROR;
           throw new Error("Sync failed");
         }
       } catch (e) {
         if (e.message == INVALID_SIGNATURE) {
           // Signature verification failed during synchronzation.
@@ -261,46 +263,73 @@ class RemoteSettingsClient {
           // remote collection.
           const payload = await fetchRemoteCollection(remote, collection);
           try {
             await this._validateCollectionSignature(remote, payload, collection, {ignoreLocal: true});
           } catch (e) {
             reportStatus = UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR;
             throw e;
           }
-          // if the signature is good (we haven't thrown), and the remote
-          // last_modified is newer than the local last_modified, replace the
-          // local data
+
+          // The signature is good (we haven't thrown).
+          // Now we will Inspect what we had locally.
+          const { data: oldData } = await collection.list();
+
+          // We build a sync result as if a diff-based sync was performed.
+          syncResult = { created: [], updated: [], deleted: [] };
+
+          // If the remote last_modified is newer than the local last_modified,
+          // replace the local data
           const localLastModified = await collection.db.getLastModified();
           if (payload.last_modified >= localLastModified) {
+            const { data: newData } = payload;
             await collection.clear();
-            await collection.loadDump(payload.data);
+            await collection.loadDump(newData);
+
+            // Compare local and remote to populate the sync result
+            const oldById = new Map(oldData.map(e => [e.id, e]));
+            for (const r of newData) {
+              const old = oldById.get(r.id);
+              if (old) {
+                if (old.last_modified != r.last_modified) {
+                  syncResult.updated.push({ old, new: r });
+                }
+                oldById.delete(r.id);
+              } else {
+                syncResult.created.push(r);
+              }
+            }
+            // Records that remain in our map now are those missing from remote
+            syncResult.deleted = Array.from(oldById.values());
           }
+
         } else {
           // The sync has thrown, it can be a network or a general error.
           if (/NetworkError/.test(e.message)) {
             reportStatus = UptakeTelemetry.STATUS.NETWORK_ERROR;
           } else if (/Backoff/.test(e.message)) {
             reportStatus = UptakeTelemetry.STATUS.BACKOFF;
           } else {
             reportStatus = UptakeTelemetry.STATUS.SYNC_ERROR;
           }
           throw e;
         }
       }
       // Read local collection of records.
-      const { data } = await collection.list();
+      const { data: current } = await collection.list();
 
       // Handle the obtained records (ie. apply locally).
       try {
         // Execute callbacks in order and sequentially.
         // If one fails everything fails.
-        const callbacks = this._callbacks.get("change");
+        const { created, updated, deleted } = syncResult;
+        const event = { data: { current, created, updated, deleted } };
+        const callbacks = this._callbacks.get("sync");
         for (const cb of callbacks) {
-          await cb({ data });
+          await cb(event);
         }
       } catch (e) {
         reportStatus = UptakeTelemetry.STATUS.APPLY_ERROR;
         throw e;
       }
 
       // Track last update.
       this._updateLastCheck(serverTime);
--- a/services/common/tests/unit/test_blocklist_clients.js
+++ b/services/common/tests/unit/test_blocklist_clients.js
@@ -1,18 +1,16 @@
 const { Constructor: CC } = Components;
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://testing-common/httpd.js");
-ChromeUtils.import("resource://gre/modules/Timer.jsm");
 const { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm", {});
 const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm", {});
 
 const BlocklistClients = ChromeUtils.import("resource://services-common/blocklist-clients.js", {});
-const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js", {});
 
 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
   "nsIBinaryInputStream", "setInputStream");
 
 
 let gBlocklistClients;
 let server;
 
@@ -62,27 +60,27 @@ function run_test() {
 
   // Setup server fake responses.
   function handleResponse(request, response) {
     try {
       const sample = getSampleResponse(request, server.identity.primaryPort);
       if (!sample) {
         do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
       }
+      const { status: { status, statusText }, sampleHeaders, responseBody } = sample;
 
-      response.setStatusLine(null, sample.status.status,
-                             sample.status.statusText);
+      response.setStatusLine(null, status, statusText);
       // send the headers
-      for (let headerLine of sample.sampleHeaders) {
-        let headerElements = headerLine.split(":");
+      for (const headerLine of sampleHeaders) {
+        const headerElements = headerLine.split(":");
         response.setHeader(headerElements[0], headerElements[1].trimLeft());
       }
       response.setHeader("Date", (new Date()).toUTCString());
 
-      response.write(sample.responseBody);
+      response.write(responseBody);
       response.finish();
     } catch (e) {
       info(e);
     }
   }
   const configPath = "/v1/";
   const addonsRecordsPath  = "/v1/buckets/blocklists/collections/addons/records";
   const gfxRecordsPath     = "/v1/buckets/blocklists/collections/gfx/records";
@@ -107,42 +105,16 @@ add_task(async function test_initial_dum
 
     // Verify the loaded data has status to synced:
     const list = await client.get();
     equal(list[0]._status, "synced");
   }
 });
 add_task(clear_state);
 
-add_task(async function test_records_obtained_from_server_are_stored_in_db() {
-  for (let {client} of gBlocklistClients) {
-    // Test an empty db populates
-    await client.maybeSync(2000, Date.now(), { loadDump: false });
-
-    // Open the collection, verify it's been populated:
-    // Our test data has a single record; it should be in the local collection
-    const list = await client.get();
-    equal(list.length, 1);
-  }
-});
-add_task(clear_state);
-
-add_task(async function test_records_changes_are_overwritten_by_server_changes() {
-  const {client} = gBlocklistClients[0];
-
-  // Create some local conflicting data, and make sure it syncs without error.
-  const collection = await client.openCollection();
-  await collection.create({
-    "versionRange": [],
-    "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
-  }, { useRecordId: true });
-  await client.maybeSync(2000, Date.now(), {loadDump: false});
-});
-add_task(clear_state);
-
 add_task(async function test_list_is_written_to_file_in_profile() {
   for (let {client, testData} of gBlocklistClients) {
     const filePath = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
     const profFile = new FileUtils.File(filePath);
     strictEqual(profFile.exists(), false);
 
     await client.maybeSync(2000, Date.now(), {loadDump: false});
 
@@ -194,123 +166,31 @@ add_task(async function test_sends_reloa
       client.maybeSync(2000, Date.now() - 1000, {loadDump: false});
     });
 
     equal(received.data.filename, client.filename);
   }
 });
 add_task(clear_state);
 
-add_task(async function test_telemetry_reports_up_to_date() {
-  for (let {client} of gBlocklistClients) {
-    await client.maybeSync(2000, Date.now() - 1000, {loadDump: false});
-    const filePath = OS.Path.join(OS.Constants.Path.profileDir, client.filename);
-    const profFile = new FileUtils.File(filePath);
-    const fileLastModified = profFile.lastModifiedTime = profFile.lastModifiedTime - 1000;
-    const serverTime = Date.now();
-    const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
-
-    await client.maybeSync(3000, serverTime);
-
-    // File was not updated.
-    equal(fileLastModified, profFile.lastModifiedTime);
-    // Server time was updated.
-    const after = Services.prefs.getIntPref(client.lastCheckTimePref);
-    equal(after, Math.round(serverTime / 1000));
-    // No Telemetry was sent.
-    const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
-    const expectedIncrements = {[UptakeTelemetry.STATUS.UP_TO_DATE]: 1};
-    checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
-  }
-});
-add_task(clear_state);
-
-add_task(async function test_telemetry_if_sync_succeeds() {
-  // We test each client because Telemetry requires preleminary declarations.
-  for (let {client} of gBlocklistClients) {
-    const serverTime = Date.now();
-    const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
-
-    await client.maybeSync(2000, serverTime, {loadDump: false});
-
-    const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
-    const expectedIncrements = {[UptakeTelemetry.STATUS.SUCCESS]: 1};
-    checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
-  }
-});
-add_task(clear_state);
-
-add_task(async function test_telemetry_reports_if_application_fails() {
-  const {client} = gBlocklistClients[0];
-  const serverTime = Date.now();
-  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
-  client.on("change", () => { throw new Error("boom"); });
-
-  try {
-    await client.maybeSync(2000, serverTime, {loadDump: false});
-  } catch (e) {}
-
-  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
-  const expectedIncrements = {[UptakeTelemetry.STATUS.APPLY_ERROR]: 1};
-  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
-});
-add_task(clear_state);
-
-add_task(async function test_telemetry_reports_if_sync_fails() {
-  const {client} = gBlocklistClients[0];
-  const serverTime = Date.now();
-
-  const collection = await client.openCollection();
-  await collection.db.saveLastModified(9999);
-
-  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
-
-  try {
-    await client.maybeSync(10000, serverTime);
-  } catch (e) {}
-
-  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
-  const expectedIncrements = {[UptakeTelemetry.STATUS.SYNC_ERROR]: 1};
-  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
-});
-add_task(clear_state);
-
-add_task(async function test_telemetry_reports_unknown_errors() {
-  const {client} = gBlocklistClients[0];
-  const serverTime = Date.now();
-  const backup = client.openCollection;
-  client.openCollection = () => { throw new Error("Internal"); };
-  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
-
-  try {
-    await client.maybeSync(2000, serverTime);
-  } catch (e) {}
-
-  client.openCollection = backup;
-  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
-  const expectedIncrements = {[UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1};
-  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
-});
-add_task(clear_state);
-
 // get a response for a given request from sample data
 function getSampleResponse(req, port) {
   const responses = {
     "OPTIONS": {
       "sampleHeaders": [
         "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
         "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
         "Access-Control-Allow-Origin: *",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress"
       ],
       "status": {status: 200, statusText: "OK"},
       "responseBody": "null"
     },
-    "GET:/v1/?": {
+    "GET:/v1/": {
       "sampleHeaders": [
         "Access-Control-Allow-Origin: *",
         "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
         "Content-Type: application/json; charset=UTF-8",
         "Server: waitress"
       ],
       "status": {status: 200, statusText: "OK"},
       "responseBody": JSON.stringify({
@@ -481,28 +361,15 @@ function getSampleResponse(req, port) {
         "blockID": "g200",
         "feature": "WEBGL_MSAA",
         "devices": [],
         "id": "c3a15ba9-e0e2-421f-e399-c995e5b8d14e",
         "last_modified": 3500,
         "os": "Darwin 11",
         "featureStatus": "BLOCKED_DEVICE"
       }]})
-    },
-    "GET:/v1/buckets/blocklists/collections/addons/records?_sort=-last_modified&_since=9999": {
-      "sampleHeaders": [
-        "Access-Control-Allow-Origin: *",
-        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
-        "Content-Type: application/json; charset=UTF-8",
-        "Server: waitress",
-      ],
-      "status": {status: 503, statusText: "Service Unavailable"},
-      "responseBody": JSON.stringify({
-        code: 503,
-        errno: 999,
-        error: "Service Unavailable",
-      })
     }
   };
   return responses[`${req.method}:${req.path}?${req.queryString}`] ||
+         responses[`${req.method}:${req.path}`] ||
          responses[req.method];
 
 }
--- a/services/common/tests/unit/test_blocklist_signatures.js
+++ b/services/common/tests/unit/test_blocklist_signatures.js
@@ -289,16 +289,17 @@ add_task(async function test_check_signa
   await OneCRLBlocklistClient.maybeSync(1000, startTime, {loadDump: false});
 
   let endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
 
   // ensure that a success histogram is tracked when a succesful sync occurs.
   let expectedIncrements = {[UptakeTelemetry.STATUS.SUCCESS]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 
+
   // Check that some additions (2 records) to the collection have a valid
   // signature.
 
   // This response adds two entries (RECORD1 and RECORD2) to the collection
   const RESPONSE_TWO_ADDED = {
     comment: "RESPONSE_TWO_ADDED",
     sampleHeaders: [
         "Content-Type: application/json; charset=UTF-8",
@@ -320,16 +321,17 @@ add_task(async function test_check_signa
     "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=1000":
       [RESPONSE_TWO_ADDED],
     "GET:/v1/buckets/blocklists/collections/certificates?":
       [RESPONSE_META_TWO_ITEMS_SIG]
   };
   registerHandlers(twoItemsResponses);
   await OneCRLBlocklistClient.maybeSync(3000, startTime);
 
+
   // Check the collection with one addition and one removal has a valid
   // signature
 
   // Remove RECORD1, add RECORD3
   const RESPONSE_ONE_ADDED_ONE_REMOVED = {
     comment: "RESPONSE_ONE_ADDED_ONE_REMOVED ",
     sampleHeaders: [
       "Content-Type: application/json; charset=UTF-8",
@@ -373,16 +375,17 @@ add_task(async function test_check_signa
     "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
       [RESPONSE_EMPTY_NO_UPDATE],
     "GET:/v1/buckets/blocklists/collections/certificates?":
       [RESPONSE_META_THREE_ITEMS_SIG]
   };
   registerHandlers(noOpResponses);
   await OneCRLBlocklistClient.maybeSync(4100, startTime);
 
+
   // Check the collection is reset when the signature is invalid
 
   // Prepare a (deliberately) bad signature to check the collection state is
   // reset if something is inconsistent
   const RESPONSE_COMPLETE_INITIAL = {
     comment: "RESPONSE_COMPLETE_INITIAL ",
     sampleHeaders: [
       "Content-Type: application/json; charset=UTF-8",
@@ -428,26 +431,37 @@ add_task(async function test_check_signa
     "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
       [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
   };
 
   registerHandlers(badSigGoodSigResponses);
 
   startHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
 
+  let retrySyncData;
+  OneCRLBlocklistClient.on("sync", ({ data }) => { retrySyncData = data; });
+
   await OneCRLBlocklistClient.maybeSync(5000, startTime);
 
   endHistogram = getUptakeTelemetrySnapshot(TELEMETRY_HISTOGRAM_KEY);
 
+  // since we only fixed the signature, and no data was changed, the sync result
+  // will be called with empty lists of created/updated/deleted.
+  equal(retrySyncData.current.length, 2);
+  equal(retrySyncData.created.length, 0);
+  equal(retrySyncData.updated.length, 0);
+  equal(retrySyncData.deleted.length, 0);
+
   // ensure that the failure count is incremented for a succesful sync with an
   // (initial) bad signature - only SERVICES_SETTINGS_SYNC_SIG_FAIL should
   // increment.
   expectedIncrements = {[UptakeTelemetry.STATUS.SIGNATURE_ERROR]: 1};
   checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
 
+
   const badSigGoodOldResponses = {
     // In this test, we deliberately serve a bad signature initially. The
     // subsequent sitnature returned is a valid one for the three item
     // collection.
     "GET:/v1/buckets/blocklists/collections/certificates?":
       [RESPONSE_META_BAD_SIG, RESPONSE_META_EMPTY_SIG],
     // The first collection state is the current state (since there's no update
     // - but, since the signature is wrong, another request will be made)
@@ -460,18 +474,72 @@ add_task(async function test_check_signa
     "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
       [RESPONSE_EMPTY_INITIAL],
   };
 
   // ensure our collection hasn't been replaced with an older, empty one
   await checkRecordCount(OneCRLBlocklistClient, 2);
 
   registerHandlers(badSigGoodOldResponses);
+
+  let oldChangesData;
+  OneCRLBlocklistClient.on("sync", ({ data }) => { oldChangesData = data; });
+
   await OneCRLBlocklistClient.maybeSync(5000, startTime);
 
+  // Local data was unchanged, since it was never than the one returned by the server.
+  equal(oldChangesData.current.length, 2);
+  equal(oldChangesData.created.length, 0);
+  equal(oldChangesData.updated.length, 0);
+  equal(oldChangesData.deleted.length, 0);
+
+
+  const badLocalContentGoodSigResponses = {
+    // In this test, we deliberately serve a bad signature initially. The
+    // subsequent signature returned is a valid one for the three item
+    // collection.
+    "GET:/v1/buckets/blocklists/collections/certificates?":
+      [RESPONSE_META_BAD_SIG, RESPONSE_META_THREE_ITEMS_SIG],
+    // The next request is for the full collection. This will be checked
+    // against the valid signature - so the sync should succeed.
+    "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified":
+      [RESPONSE_COMPLETE_INITIAL],
+    // The next request is for the full collection sorted by id. This will be
+    // checked against the valid signature - so the sync should succeed.
+    "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=id":
+      [RESPONSE_COMPLETE_INITIAL_SORTED_BY_ID]
+  };
+
+  registerHandlers(badLocalContentGoodSigResponses);
+
+  // we create a local state manually here, in order to test that the sync event data
+  // properly contains created, updated, and deleted records.
+  // the final server collection contains RECORD2 and RECORD3
+  const kintoCol = await OneCRLBlocklistClient.openCollection();
+  await kintoCol.clear();
+  await kintoCol.create({ ...RECORD2, last_modified: 1234567890, serialNumber: "abc" }, { synced: true, useRecordId: true });
+  const localId = "0602b1b2-12ab-4d3a-b6fb-593244e7b035";
+  await kintoCol.create({ id: localId }, { synced: true, useRecordId: true });
+
+  let syncData;
+  OneCRLBlocklistClient.on("sync", ({ data }) => { syncData = data; });
+
+  await OneCRLBlocklistClient.maybeSync(5000, startTime, { loadDump: false });
+
+  // Local data was unchanged, since it was never than the one returned by the server.
+  equal(syncData.current.length, 2);
+  equal(syncData.created.length, 1);
+  equal(syncData.created[0].id, RECORD3.id);
+  equal(syncData.updated.length, 1);
+  equal(syncData.updated[0].old.serialNumber, "abc");
+  equal(syncData.updated[0].new.serialNumber, RECORD2.serialNumber);
+  equal(syncData.deleted.length, 1);
+  equal(syncData.deleted[0].id, localId);
+
+
   const allBadSigResponses = {
     // In this test, we deliberately serve only a bad signature.
     "GET:/v1/buckets/blocklists/collections/certificates?":
       [RESPONSE_META_BAD_SIG],
     // The first collection state is the three item collection (since
     // there's a sync with no updates) - but, since the signature is wrong,
     // another request will be made...
     "GET:/v1/buckets/blocklists/collections/certificates/records?_sort=-last_modified&_since=4000":
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_remote_settings.js
@@ -0,0 +1,311 @@
+const { Constructor: CC } = Components;
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://testing-common/httpd.js");
+
+const { RemoteSettings } = ChromeUtils.import("resource://services-common/remote-settings.js", {});
+const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js", {});
+
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+  "nsIBinaryInputStream", "setInputStream");
+
+let server;
+let client;
+
+async function clear_state() {
+  // Clear local DB.
+  const collection = await client.openCollection();
+  await collection.clear();
+}
+
+
+function run_test() {
+  // Set up an HTTP Server
+  server = new HttpServer();
+  server.start(-1);
+
+  // Point the blocklist clients to use this local HTTP server.
+  Services.prefs.setCharPref("services.settings.server",
+                             `http://localhost:${server.identity.primaryPort}/v1`);
+  // Ensure that signature verification is disabled to prevent interference
+  // with basic certificate sync tests
+  Services.prefs.setBoolPref("services.settings.verify_signature", false);
+
+  client = RemoteSettings("password-fields");
+
+  // Setup server fake responses.
+  function handleResponse(request, response) {
+    try {
+      const sample = getSampleResponse(request, server.identity.primaryPort);
+      if (!sample) {
+        do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
+      }
+
+      response.setStatusLine(null, sample.status.status,
+                             sample.status.statusText);
+      // send the headers
+      for (let headerLine of sample.sampleHeaders) {
+        let headerElements = headerLine.split(":");
+        response.setHeader(headerElements[0], headerElements[1].trimLeft());
+      }
+      response.setHeader("Date", (new Date()).toUTCString());
+
+      response.write(JSON.stringify(sample.responseBody));
+      response.finish();
+    } catch (e) {
+      info(e);
+    }
+  }
+  const configPath = "/v1/";
+  const recordsPath  = "/v1/buckets/main/collections/password-fields/records";
+  server.registerPathHandler(configPath, handleResponse);
+  server.registerPathHandler(recordsPath, handleResponse);
+
+  run_next_test();
+
+  registerCleanupFunction(function() {
+    server.stop(() => { });
+  });
+}
+
+add_task(async function test_records_obtained_from_server_are_stored_in_db() {
+  // Test an empty db populates
+  await client.maybeSync(2000, Date.now());
+
+  // Open the collection, verify it's been populated:
+  // Our test data has a single record; it should be in the local collection
+  const list = await client.get();
+  equal(list.length, 1);
+});
+add_task(clear_state);
+
+add_task(async function test_records_changes_are_overwritten_by_server_changes() {
+  // Create some local conflicting data, and make sure it syncs without error.
+  const collection = await client.openCollection();
+  await collection.create({
+    "website": "",
+    "id": "9d500963-d80e-3a91-6e74-66f3811b99cc"
+  }, { useRecordId: true });
+
+  await client.maybeSync(2000, Date.now());
+
+  const data = await client.get();
+  equal(data[0].website, "https://some-website.com");
+});
+add_task(clear_state);
+
+add_task(async function test_sync_event_provides_information_about_records() {
+  const serverTime = Date.now();
+
+  let eventData;
+  client.on("sync", ({ data }) => eventData = data);
+
+  await client.maybeSync(2000, serverTime - 1000);
+  equal(eventData.current.length, 1);
+
+  await client.maybeSync(3001, serverTime);
+  equal(eventData.current.length, 2);
+  equal(eventData.created.length, 1);
+  equal(eventData.created[0].website, "https://www.other.org/signin");
+  equal(eventData.updated.length, 1);
+  equal(eventData.updated[0].old.website, "https://some-website.com");
+  equal(eventData.updated[0].new.website, "https://some-website.com/login");
+  equal(eventData.deleted.length, 0);
+
+  await client.maybeSync(4001, serverTime);
+  equal(eventData.current.length, 1);
+  equal(eventData.created.length, 0);
+  equal(eventData.updated.length, 0);
+  equal(eventData.deleted.length, 1);
+  equal(eventData.deleted[0].website, "https://www.other.org/signin");
+});
+add_task(clear_state);
+
+add_task(async function test_telemetry_reports_up_to_date() {
+  await client.maybeSync(2000, Date.now() - 1000);
+  const serverTime = Date.now();
+  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
+
+  await client.maybeSync(3000, serverTime);
+
+  // No Telemetry was sent.
+  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
+  const expectedIncrements = {[UptakeTelemetry.STATUS.UP_TO_DATE]: 1};
+  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
+});
+add_task(clear_state);
+
+add_task(async function test_telemetry_if_sync_succeeds() {
+  // We test each client because Telemetry requires preleminary declarations.
+  const serverTime = Date.now();
+  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
+
+  await client.maybeSync(2000, serverTime);
+
+  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
+  const expectedIncrements = {[UptakeTelemetry.STATUS.SUCCESS]: 1};
+  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
+});
+add_task(clear_state);
+
+add_task(async function test_telemetry_reports_if_application_fails() {
+  const serverTime = Date.now();
+  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
+  client.on("sync", () => { throw new Error("boom"); });
+
+  try {
+    await client.maybeSync(2000, serverTime);
+  } catch (e) {}
+
+  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
+  const expectedIncrements = {[UptakeTelemetry.STATUS.APPLY_ERROR]: 1};
+  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
+});
+add_task(clear_state);
+
+add_task(async function test_telemetry_reports_if_sync_fails() {
+  const serverTime = Date.now();
+
+  const collection = await client.openCollection();
+  await collection.db.saveLastModified(9999);
+
+  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
+
+  try {
+    await client.maybeSync(10000, serverTime);
+  } catch (e) {}
+
+  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
+  const expectedIncrements = {[UptakeTelemetry.STATUS.SYNC_ERROR]: 1};
+  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
+});
+add_task(clear_state);
+
+add_task(async function test_telemetry_reports_unknown_errors() {
+  const serverTime = Date.now();
+  const backup = client.openCollection;
+  client.openCollection = () => { throw new Error("Internal"); };
+  const startHistogram = getUptakeTelemetrySnapshot(client.identifier);
+
+  try {
+    await client.maybeSync(2000, serverTime);
+  } catch (e) {}
+
+  client.openCollection = backup;
+  const endHistogram = getUptakeTelemetrySnapshot(client.identifier);
+  const expectedIncrements = {[UptakeTelemetry.STATUS.UNKNOWN_ERROR]: 1};
+  checkUptakeTelemetry(startHistogram, endHistogram, expectedIncrements);
+});
+add_task(clear_state);
+
+// get a response for a given request from sample data
+function getSampleResponse(req, port) {
+  const responses = {
+    "OPTIONS": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
+        "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
+        "Access-Control-Allow-Origin: *",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress"
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": null
+    },
+    "GET:/v1/": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress"
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": {
+        "settings": {
+          "batch_max_requests": 25
+        },
+        "url": `http://localhost:${port}/v1/`,
+        "documentation": "https://kinto.readthedocs.org/",
+        "version": "1.5.1",
+        "commit": "cbc6f58",
+        "hello": "kinto"
+      }
+    },
+    "GET:/v1/buckets/main/collections/password-fields/records?_sort=-last_modified": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"3000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": {
+        "data": [{
+          "id": "9d500963-d80e-3a91-6e74-66f3811b99cc",
+          "last_modified": 3000,
+          "website": "https://some-website.com",
+          "selector": "#user[password]"
+        }]
+      }
+    },
+    "GET:/v1/buckets/main/collections/password-fields/records?_sort=-last_modified&_since=3000": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"4000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": {
+        "data": [{
+          "id": "aabad965-e556-ffe7-4191-074f5dee3df3",
+          "last_modified": 4000,
+          "website": "https://www.other.org/signin",
+          "selector": "#signinpassword"
+        }, {
+          "id": "9d500963-d80e-3a91-6e74-66f3811b99cc",
+          "last_modified": 3500,
+          "website": "https://some-website.com/login",
+          "selector": "input#user[password]"
+        }]
+      }
+    },
+    "GET:/v1/buckets/main/collections/password-fields/records?_sort=-last_modified&_since=4000": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+        "Etag: \"5000\""
+      ],
+      "status": {status: 200, statusText: "OK"},
+      "responseBody": {
+        "data": [{
+          "id": "aabad965-e556-ffe7-4191-074f5dee3df3",
+          "deleted": true
+        }]
+      }
+    },
+    "GET:/v1/buckets/main/collections/password-fields/records?_sort=-last_modified&_since=9999": {
+      "sampleHeaders": [
+        "Access-Control-Allow-Origin: *",
+        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
+        "Content-Type: application/json; charset=UTF-8",
+        "Server: waitress",
+      ],
+      "status": {status: 503, statusText: "Service Unavailable"},
+      "responseBody": {
+        code: 503,
+        errno: 999,
+        error: "Service Unavailable",
+      }
+    }
+  };
+  dump(`${req.method}:${req.path}?${req.queryString}`);
+  return responses[`${req.method}:${req.path}?${req.queryString}`] ||
+         responses[`${req.method}:${req.path}`] ||
+         responses[req.method];
+
+}
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -11,16 +11,18 @@ support-files =
 [test_blocklist_certificates.js]
 # Initial JSON data for blocklists are not shipped on Android.
 skip-if = (os == "android" || appname == "thunderbird")
 tags = blocklist
 [test_blocklist_clients.js]
 tags = blocklist
 [test_blocklist_pinning.js]
 tags = blocklist
+[test_remote_settings.js]
+tags = remote-settings blocklist
 [test_remote_settings_poll.js]
 tags = remote-settings blocklist
 
 [test_kinto.js]
 tags = blocklist
 [test_blocklist_signatures.js]
 tags = remote-settings blocklist
 [test_storage_adapter.js]
--- a/taskcluster/ci/release-update-verify-config/kind.yml
+++ b/taskcluster/ci/release-update-verify-config/kind.yml
@@ -56,19 +56,16 @@ job-defaults:
             birch: nonbeta
             jamun: beta
             maple: beta
             mozilla-beta: beta
             mozilla-release: nonbeta
             default: null
       last-watershed:
          by-project:
-            # TODO: add esr here when setting up mozilla-esr60
-            # let's put mozilla-esr52 in this comment as well, in case
-            # somebody is grepping the tree for things they need to do.
             birch:
                by-build-platform:
                   linux*: "57.0"
                   linux64*: "57.0"
                   macosx64*: "57.0"
                   win32*: "56.0"
                   win64*: "56.0"
                   default: null
@@ -78,16 +75,24 @@ job-defaults:
             mozilla-release:
                by-build-platform:
                   linux*: "57.0"
                   linux64*: "57.0"
                   macosx64*: "57.0"
                   win32*: "56.0"
                   win64*: "56.0"
                   default: null
+            mozilla-esr52:
+               by-build-platform:
+                  macosx64: "45.9.0esr"
+                  win32: "45.3.0esr"
+                  win64: "45.3.0esr"
+                  default: null
+            # TODO add bz2 -> lzma watershed when we ship the first ESR 52 -> 60 update
+            mozilla-esr60: "60.0esr"
             default: null
 
 jobs:
    firefox-linux:
       shipping-product: firefox
       treeherder:
          symbol: UVC
          platform: linux/opt
--- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm
@@ -4,16 +4,20 @@
 
 /*
  * This module implements a number of utilities useful for browser tests.
  *
  * All asynchronous helper methods should return promises, rather than being
  * callback based.
  */
 
+// This file uses ContentTask & frame scripts, where these are available.
+/* global addEventListener, removeEventListener, sendAsyncMessage,
+          addMessageListener, removeMessageListener, privateNoteIntentionalCrash */
+
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "BrowserTestUtils",
 ];
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -92,17 +96,17 @@ var BrowserTestUtils = {
    * @resolves When the tab has been closed.
    * @rejects Any exception from taskFn is propagated.
    */
   async withNewTab(options, taskFn) {
     if (typeof(options) == "string") {
       options = {
         gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
         url: options
-      }
+      };
     }
     let tab = await BrowserTestUtils.openNewForegroundTab(options);
     let originalWindow = tab.ownerGlobal;
     let result = await taskFn(tab.linkedBrowser);
     let finalWindow = tab.ownerGlobal;
     if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) {
       // taskFn may resolve within a tick after opening a new tab.
       // We shouldn't remove the newly opened tab in the same tick.
@@ -182,22 +186,21 @@ var BrowserTestUtils = {
       // If DEFAULT_PROCESSSELECTOR_CID is null, we're in non-e10s mode and we
       // should skip this.
       if (options.forceNewProcess && DEFAULT_PROCESSSELECTOR_CID) {
         registrar.registerFactory(OUR_PROCESSSELECTOR_CID, "",
                                   PROCESSSELECTOR_CONTRACTID, null);
       }
 
       promises = [
-        BrowserTestUtils.switchTab(tabbrowser, function () {
+        BrowserTestUtils.switchTab(tabbrowser, function() {
           if (typeof opening == "function") {
             opening();
             tab = tabbrowser.selectedTab;
-          }
-          else {
+          } else {
             tabbrowser.selectedTab = tab = tabbrowser.addTab(opening);
           }
         })
       ];
 
       if (aWaitForLoad) {
         promises.push(BrowserTestUtils.browserLoaded(tab.linkedBrowser));
       }
@@ -230,18 +233,17 @@ var BrowserTestUtils = {
     let promise = new Promise(resolve => {
       tabbrowser.addEventListener("TabSwitchDone", function() {
         TestUtils.executeSoon(() => resolve(tabbrowser.selectedTab));
       }, {once: true});
     });
 
     if (typeof tab == "function") {
       tab();
-    }
-    else {
+    } else {
       tabbrowser.selectedTab = tab;
     }
     return promise;
   },
 
   /**
    * Waits for an ongoing page load in a browser window to complete.
    *
@@ -262,39 +264,39 @@ var BrowserTestUtils = {
    * @param {optional boolean} maybeErrorPage
    *        If true, this uses DOMContentLoaded event instead of load event.
    *        Also wantLoad will be called with visible URL, instead of
    *        'about:neterror?...' for error page.
    *
    * @return {Promise}
    * @resolves When a load event is triggered for the browser.
    */
-  browserLoaded(browser, includeSubFrames=false, wantLoad=null,
-                maybeErrorPage=false) {
+  browserLoaded(browser, includeSubFrames = false, wantLoad = null,
+                maybeErrorPage = false) {
     // Passing a url as second argument is a common mistake we should prevent.
     if (includeSubFrames && typeof includeSubFrames != "boolean") {
-      throw("The second argument to browserLoaded should be a boolean.");
+      throw ("The second argument to browserLoaded should be a boolean.");
     }
 
     // If browser belongs to tabbrowser-tab, ensure it has been
     // inserted into the document.
     let tabbrowser = browser.ownerGlobal.gBrowser;
     if (tabbrowser && tabbrowser.getTabForBrowser) {
       tabbrowser._insertBrowser(tabbrowser.getTabForBrowser(browser));
     }
 
     function isWanted(url) {
       if (!wantLoad) {
         return true;
       } else if (typeof(wantLoad) == "function") {
         return wantLoad(url);
-      } else {
+      }
         // It's a string.
         return wantLoad == url;
-      }
+
     }
 
     return new Promise(resolve => {
       let mm = browser.ownerGlobal.messageManager;
       let eventName = maybeErrorPage
           ? "browser-test-utils:DOMContentLoadedEvent"
           : "browser-test-utils:loadEvent";
       mm.addMessageListener(eventName, function onLoad(msg) {
@@ -333,17 +335,17 @@ var BrowserTestUtils = {
     let mm = win.messageManager;
     return this.waitForMessage(mm, "browser-test-utils:loadEvent", (msg) => {
       if (checkFn) {
         return checkFn(msg.target);
       }
 
       let selectedBrowser = win.gBrowser.selectedBrowser;
       return msg.target == selectedBrowser &&
-             (aboutBlank || selectedBrowser.currentURI.spec != "about:blank")
+             (aboutBlank || selectedBrowser.currentURI.spec != "about:blank");
     });
   },
 
   _webProgressListeners: new Set(),
 
   /**
    * Waits for the web progress listener associated with this tab to fire a
    * STATE_STOP for the toplevel document.
@@ -354,35 +356,33 @@ var BrowserTestUtils = {
    *        A specific URL to check the channel load against
    * @param {Boolean} checkAborts (optional, defaults to false)
    *        Whether NS_BINDING_ABORTED stops 'count' as 'real' stops
    *        (e.g. caused by the stop button or equivalent APIs)
    *
    * @return {Promise}
    * @resolves When STATE_STOP reaches the tab's progress listener
    */
-  browserStopped(browser, expectedURI, checkAborts=false) {
+  browserStopped(browser, expectedURI, checkAborts = false) {
     return new Promise(resolve => {
-      const kDocStopFlags = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
-                            Ci.nsIWebProgressListener.STATE_STOP;
       let wpl = {
         onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
           dump("Saw state " + aStateFlags.toString(16) + " and status " + aStatus.toString(16) + "\n");
           if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
               aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
               (checkAborts || aStatus != Cr.NS_BINDING_ABORTED) &&
               aWebProgress.isTopLevel) {
             let chan = aRequest.QueryInterface(Ci.nsIChannel);
             dump("Browser loaded " + chan.originalURI.spec + "\n");
             if (!expectedURI || chan.originalURI.spec == expectedURI) {
               browser.removeProgressListener(wpl);
               BrowserTestUtils._webProgressListeners.delete(wpl);
               resolve();
             }
-          };
+          }
         },
         onSecurityChange() {},
         onStatusChange() {},
         onLocationChange() {},
         QueryInterface: ChromeUtils.generateQI([
           Ci.nsIWebProgressListener,
           Ci.nsIWebProgressListener2,
           Ci.nsISupportsWeakReference,
@@ -660,17 +660,17 @@ var BrowserTestUtils = {
    *                   remote browser tabs or not. If omitted, the window
    *                   will choose the profile default state.
    *          width: Desired width of window
    *          height: Desired height of window
    *        }
    * @return {Promise}
    *         Resolves with the new window once it is loaded.
    */
-  async openNewBrowserWindow(options={}) {
+  async openNewBrowserWindow(options = {}) {
     let argString = Cc["@mozilla.org/supports-string;1"].
                     createInstance(Ci.nsISupportsString);
     argString.data = "";
     let features = "chrome,dialog=no,all";
     let opener = null;
 
     if (options.opener) {
       opener = options.opener;
@@ -746,29 +746,29 @@ var BrowserTestUtils = {
    * @param {Window}
    *        The closing window.
    *
    * @return {Promise}
    *        Resolves when the provided window has been fully closed. For
    *        browser windows, the Promise will also wait until all final
    *        SessionStore messages have been sent up from all browser tabs.
    */
-  windowClosed(win)  {
+  windowClosed(win) {
     let domWinClosedPromise = BrowserTestUtils.domWindowClosed(win);
     let promises = [domWinClosedPromise];
     let winType = win.document.documentElement.getAttribute("windowtype");
 
     if (winType == "navigator:browser") {
       let finalMsgsPromise = new Promise((resolve) => {
         let browserSet = new Set(win.gBrowser.browsers);
         // Ensure all browsers have been inserted or we won't get
         // messages back from them.
         browserSet.forEach((browser) => {
           win.gBrowser._insertBrowser(win.gBrowser.getTabForBrowser(browser));
-        })
+        });
         let mm = win.getGroupMessageManager("browsers");
 
         mm.addMessageListener("SessionStore:update", function onMessage(msg) {
           if (browserSet.has(msg.target) && msg.data.isFinal) {
             browserSet.delete(msg.target);
             if (!browserSet.size) {
               mm.removeMessageListener("SessionStore:update", onMessage);
               // Give the TabStateFlusher a chance to react to this final
@@ -905,16 +905,17 @@ var BrowserTestUtils = {
    */
   waitForContentEvent(browser, eventName, capture = false, checkFn, wantsUntrusted = false) {
     let parameters = {
       eventName,
       capture,
       checkFnSource: checkFn ? checkFn.toSource() : null,
       wantsUntrusted,
     };
+    /* eslint-disable no-eval */
     return ContentTask.spawn(browser, parameters,
         function({ eventName, capture, checkFnSource, wantsUntrusted }) {
           let checkFn;
           if (checkFnSource) {
             checkFn = eval(`(() => (${checkFnSource}))()`);
           }
           return new Promise((resolve, reject) => {
             addEventListener(eventName, function listener(event) {
@@ -926,16 +927,17 @@ var BrowserTestUtils = {
               } catch (e) {
                 completion = () => reject(e);
               }
               removeEventListener(eventName, listener, capture);
               completion();
             }, capture, wantsUntrusted);
           });
         });
+    /* eslint-enable no-eval */
   },
 
   /**
    * Adds a content event listener on the given browser
    * element. Similar to waitForContentEvent, but the listener will
    * fire until it is removed. A callable object is returned that,
    * when called, removes the event listener. Note that this function
    * works even if the browser's frameloader is swapped.
@@ -974,16 +976,17 @@ var BrowserTestUtils = {
                           autoremove = true) {
     let id = gListenerId++;
     let checkFnSource = checkFn ? encodeURIComponent(escape(checkFn.toSource())) : "";
 
     // To correctly handle frameloader swaps, we load a frame script
     // into all tabs but ignore messages from the ones not related to
     // |browser|.
 
+    /* eslint-disable no-eval */
     function frameScript(id, eventName, useCapture, checkFnSource, wantsUntrusted) {
       let checkFn;
       if (checkFnSource) {
         checkFn = eval(`(() => (${unescape(checkFnSource)}))()`);
       }
 
       function listener(event) {
         if (checkFn && !checkFn(event)) {
@@ -995,16 +998,17 @@ var BrowserTestUtils = {
         if (msg.data == id) {
           removeMessageListener("ContentEventListener:Remove", removeListener);
           removeEventListener(eventName, listener, useCapture, wantsUntrusted);
         }
       }
       addMessageListener("ContentEventListener:Remove", removeListener);
       addEventListener(eventName, listener, useCapture, wantsUntrusted);
     }
+    /* eslint-enable no-eval */
 
     let frameScriptSource =
         `data:,(${frameScript.toString()})(${id}, "${eventName}", ${useCapture}, "${checkFnSource}", ${wantsUntrusted})`;
 
     let mm = Services.mm;
 
     function runListener(msg) {
       if (msg.data == id && msg.target == browser) {
@@ -1098,18 +1102,17 @@ var BrowserTestUtils = {
    * @param {Object} event object
    *        Additional arguments, similar to the EventUtils.jsm version
    * @param {Browser} browser
    *        Browser element, must not be null
    *
    * @returns {Promise}
    * @resolves True if the mouse event was cancelled.
    */
-  synthesizeMouse(target, offsetX, offsetY, event, browser)
-  {
+  synthesizeMouse(target, offsetX, offsetY, event, browser) {
     return new Promise((resolve, reject) => {
       let mm = browser.messageManager;
       mm.addMessageListener("Test:SynthesizeMouseDone", function mouseMsg(message) {
         mm.removeMessageListener("Test:SynthesizeMouseDone", mouseMsg);
         if (message.data.hasOwnProperty("defaultPrevented")) {
           resolve(message.data.defaultPrevented);
         } else {
           reject(new Error(message.data.error));
@@ -1122,17 +1125,17 @@ var BrowserTestUtils = {
         targetFn = target.toString();
         target = null;
       } else if (typeof target != "string" && !Array.isArray(target)) {
         cpowObject = target;
         target = null;
       }
 
       mm.sendAsyncMessage("Test:SynthesizeMouse",
-                          {target, targetFn, x: offsetX, y: offsetY, event: event},
+                          {target, targetFn, x: offsetX, y: offsetY, event},
                           {object: cpowObject});
     });
   },
 
   /**
    * Wait for a message to be fired from a particular message manager
    *
    * @param {nsIMessageManager} messageManager
@@ -1152,30 +1155,28 @@ var BrowserTestUtils = {
       });
     });
   },
 
   /**
    *  Version of synthesizeMouse that uses the center of the target as the mouse
    *  location. Arguments and the return value are the same.
    */
-  synthesizeMouseAtCenter(target, event, browser)
-  {
+  synthesizeMouseAtCenter(target, event, browser) {
     // Use a flag to indicate to center rather than having a separate message.
     event.centered = true;
     return BrowserTestUtils.synthesizeMouse(target, 0, 0, event, browser);
   },
 
   /**
    *  Version of synthesizeMouse that uses a client point within the child
    *  window instead of a target as the offset. Otherwise, the arguments and
    *  return value are the same as synthesizeMouse.
    */
-  synthesizeMouseAtPoint(offsetX, offsetY, event, browser)
-  {
+  synthesizeMouseAtPoint(offsetX, offsetY, event, browser) {
     return BrowserTestUtils.synthesizeMouse(null, offsetX, offsetY, event, browser);
   },
 
   /**
    * Removes the given tab from its parent tabbrowser.
    * This method doesn't SessionStore etc.
    *
    * @param (tab) tab
@@ -1211,35 +1212,35 @@ var BrowserTestUtils = {
    *        tab crash page has loaded.
    * @param (bool) shouldClearMinidumps
    *        True if the minidumps left behind by the crash should be removed.
    *
    * @returns (Promise)
    * @resolves An Object with key-value pairs representing the data from the
    *           crash report's extra file (if applicable).
    */
-  async crashBrowser(browser, shouldShowTabCrashPage=true,
-                     shouldClearMinidumps=true) {
+  async crashBrowser(browser, shouldShowTabCrashPage = true,
+                     shouldClearMinidumps = true) {
     let extra = {};
     let KeyValueParser = {};
     if (AppConstants.MOZ_CRASHREPORTER) {
       ChromeUtils.import("resource://gre/modules/KeyValueParser.jsm", KeyValueParser);
     }
 
     if (!browser.isRemoteBrowser) {
       throw new Error("<xul:browser> needs to be remote in order to crash");
     }
 
     /**
      * Returns the directory where crash dumps are stored.
      *
      * @return nsIFile
      */
     function getMinidumpDirectory() {
-      let dir = Services.dirsvc.get('ProfD', Ci.nsIFile);
+      let dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
       dir.append("minidumps");
       return dir;
     }
 
     /**
      * Removes a file from a directory. This is a no-op if the file does not
      * exist.
      *
@@ -1262,84 +1263,86 @@ var BrowserTestUtils = {
     // frame script.
     let frame_script = () => {
       ChromeUtils.import("resource://gre/modules/ctypes.jsm");
 
       let dies = function() {
         privateNoteIntentionalCrash();
         let zero = new ctypes.intptr_t(8);
         let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
-        badptr.contents
+        badptr.contents;
       };
 
       dump("\nEt tu, Brute?\n");
       dies();
-    }
+    };
 
     let expectedPromises = [];
 
     let crashCleanupPromise = new Promise((resolve, reject) => {
       let observer = (subject, topic, data) => {
         if (topic != "ipc:content-shutdown") {
-          return reject("Received incorrect observer topic: " + topic);
+          reject("Received incorrect observer topic: " + topic);
+          return;
         }
         if (!(subject instanceof Ci.nsIPropertyBag2)) {
-          return reject("Subject did not implement nsIPropertyBag2");
+          reject("Subject did not implement nsIPropertyBag2");
+          return;
         }
         // we might see this called as the process terminates due to previous tests.
         // We are only looking for "abnormal" exits...
         if (!subject.hasKey("abnormal")) {
           dump("\nThis is a normal termination and isn't the one we are looking for...\n");
           return;
         }
 
         let dumpID;
         if (AppConstants.MOZ_CRASHREPORTER) {
-          dumpID = subject.getPropertyAsAString('dumpID');
+          dumpID = subject.getPropertyAsAString("dumpID");
           if (!dumpID) {
-            return reject("dumpID was not present despite crash reporting " +
-                          "being enabled");
+            reject("dumpID was not present despite crash reporting being enabled");
+            return;
           }
         }
 
         let removalPromise = Promise.resolve();
 
         if (dumpID) {
           removalPromise = Services.crashmanager.ensureCrashIsPresent(dumpID)
                                                 .then(() => {
             let minidumpDirectory = getMinidumpDirectory();
             let extrafile = minidumpDirectory.clone();
-            extrafile.append(dumpID + '.extra');
+            extrafile.append(dumpID + ".extra");
             if (extrafile.exists()) {
               dump(`\nNo .extra file for dumpID: ${dumpID}\n`);
               if (AppConstants.MOZ_CRASHREPORTER) {
                 extra = KeyValueParser.parseKeyValuePairsFromFile(extrafile);
               } else {
-                dump('\nCrashReporter not enabled - will not return any extra data\n');
+                dump("\nCrashReporter not enabled - will not return any extra data\n");
               }
             }
 
             if (shouldClearMinidumps) {
-              removeFile(minidumpDirectory, dumpID + '.dmp');
-              removeFile(minidumpDirectory, dumpID + '.extra');
+              removeFile(minidumpDirectory, dumpID + ".dmp");
+              removeFile(minidumpDirectory, dumpID + ".extra");
             }
           });
         }
 
         removalPromise.then(() => {
-          Services.obs.removeObserver(observer, 'ipc:content-shutdown');
+          Services.obs.removeObserver(observer, "ipc:content-shutdown");
           dump("\nCrash cleaned up\n");
           // There might be other ipc:content-shutdown handlers that need to
           // run before we want to continue, so we'll resolve on the next tick
           // of the event loop.
           TestUtils.executeSoon(() => resolve());
         });
       };
 
-      Services.obs.addObserver(observer, 'ipc:content-shutdown');
+      Services.obs.addObserver(observer, "ipc:content-shutdown");
     });
 
     expectedPromises.push(crashCleanupPromise);
 
     if (shouldShowTabCrashPage) {
       expectedPromises.push(new Promise((resolve, reject) => {
         browser.addEventListener("AboutTabCrashedReady", function onCrash() {
           browser.removeEventListener("AboutTabCrashedReady", onCrash);
@@ -1382,17 +1385,17 @@ var BrowserTestUtils = {
   waitForAttribute(attr, element, value) {
     let MutationObserver = element.ownerGlobal.MutationObserver;
     return new Promise(resolve => {
       let mut = new MutationObserver(mutations => {
         if ((!value && element.getAttribute(attr)) ||
             (value && element.getAttribute(attr) === value)) {
           resolve();
           mut.disconnect();
-          return;
+
         }
       });
 
       mut.observe(element, {attributeFilter: [attr]});
     });
   },
 
   /**
@@ -1418,18 +1421,18 @@ var BrowserTestUtils = {
         if (message.data.seq != seq)
           return;
 
         mm.removeMessageListener("Test:SendCharDone", charMsg);
         resolve(message.data.result);
       });
 
       mm.sendAsyncMessage("Test:SendChar", {
-        char: char,
-        seq: seq
+        char,
+        seq
       });
     });
   },
 
   /**
    * Version of EventUtils' `synthesizeKey` function; it will synthesize a key
    * event in a child process and returns a Promise that will resolve when the
    * event was fired. Instead of a Window, a Browser object is required to be
@@ -1592,17 +1595,17 @@ var BrowserTestUtils = {
    * @returns Promise
    */
   contentPainted(browser) {
     return ContentTask.spawn(browser, null, async function() {
       return new Promise((resolve) => {
         addEventListener("MozAfterPaint", function onPaint() {
           removeEventListener("MozAfterPaint", onPaint);
           resolve();
-        })
+        });
       });
     });
   },
 
   _knownAboutPages: new Set(),
   _loadedAboutContentScript: false,
   /**
    * Registers an about: page with particular flags in both the parent
@@ -1668,17 +1671,17 @@ var BrowserTestUtils = {
    * @param {string} uri
    *        The URI of the dialog to wait for.  Defaults to the common dialog.
    * @return {Promise}
    *         A Promise which resolves when a "domwindowopened" notification
    *         for a dialog has been fired by the window watcher and the
    *         specified button is clicked.
    */
   async promiseAlertDialogOpen(buttonAction,
-                               uri="chrome://global/content/commonDialog.xul",
+                               uri = "chrome://global/content/commonDialog.xul",
                                func) {
     let win = await this.domWindowOpened(null, async win => {
       // The test listens for the "load" event which guarantees that the alert
       // class has already been added (it is added when "DOMContentLoaded" is
       // fired).
       await this.waitForEvent(win, "load");
 
       return win.document.documentURI === uri;
@@ -1704,17 +1707,17 @@ var BrowserTestUtils = {
    * @param {string} uri
    *        The URI of the dialog to wait for.  Defaults to the common dialog.
    * @return {Promise}
    *         A Promise which resolves when a "domwindowopened" notification
    *         for a dialog has been fired by the window watcher and the
    *         specified button is clicked, and the dialog has been fully closed.
    */
   async promiseAlertDialog(buttonAction,
-                           uri="chrome://global/content/commonDialog.xul",
+                           uri = "chrome://global/content/commonDialog.xul",
                            func) {
     let win = await this.promiseAlertDialogOpen(buttonAction, uri, func);
     return this.windowClosed(win);
   },
 
   /**
    * Opens a tab with a given uri and params object. If the params object is not set
    * or the params parameter does not include a triggeringPricnipal then this function
--- a/testing/mochitest/BrowserTestUtils/ContentTask.jsm
+++ b/testing/mochitest/BrowserTestUtils/ContentTask.jsm
@@ -75,19 +75,19 @@ var ContentTask = {
     });
 
     let id = gMessageID++;
     gPromises.set(id, deferred);
 
     browser.messageManager.sendAsyncMessage(
       "content-task:spawn",
       {
-        id: id,
+        id,
         runnable: task.toString(),
-        arg: arg,
+        arg,
       });
 
     return deferred.promise;
   },
 
   setTestScope(scope) {
     this._testScope = scope;
     this._scopeValidId = gMessageID;
--- a/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm
+++ b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.jsm
@@ -31,31 +31,31 @@ var ContentTaskUtils = {
    * @param attempts
    *        The number of times to poll before giving up and rejecting
    *        if the condition has not yet returned true. Defaults to 50
    *        (~5 seconds for 100ms intervals)
    * @return Promise
    *        Resolves when condition is true.
    *        Rejects if timeout is exceeded or condition ever throws.
    */
-  waitForCondition(condition, msg, interval=100, maxTries=50) {
+  waitForCondition(condition, msg, interval = 100, maxTries = 50) {
     return new Promise((resolve, reject) => {
       let tries = 0;
       let intervalID = setInterval(() => {
         if (tries >= maxTries) {
           clearInterval(intervalID);
           msg += ` - timed out after ${maxTries} tries.`;
           reject(msg);
           return;
         }
 
         let conditionPassed = false;
         try {
           conditionPassed = condition();
-        } catch(e) {
+        } catch (e) {
           msg += ` - threw exception: ${e}`;
           clearInterval(intervalID);
           reject(msg);
           return;
         }
 
         if (conditionPassed) {
           clearInterval(intervalID);
--- a/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js
+++ b/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js
@@ -1,8 +1,10 @@
+/* eslint-env mozilla/frame-script */
+
 "use strict";
 
 var Cm = Components.manager;
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const { generateUUID } = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
--- a/testing/mochitest/BrowserTestUtils/content/content-task.js
+++ b/testing/mochitest/BrowserTestUtils/content/content-task.js
@@ -1,69 +1,74 @@
 /* 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/. */
 
+/* eslint-env mozilla/frame-script */
+
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Task.jsm", this);
 ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm", this);
 const AssertCls = ChromeUtils.import("resource://testing-common/Assert.jsm", null).Assert;
 
-addMessageListener("content-task:spawn", function (msg) {
+addMessageListener("content-task:spawn", function(msg) {
   let id = msg.data.id;
   let source = msg.data.runnable || "()=>{}";
 
   function getStack(aStack) {
     let frames = [];
     for (let frame = aStack; frame; frame = frame.caller) {
       frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
     }
     return frames.join("\n");
   }
 
   var Assert = new AssertCls((err, message, stack) => {
     sendAsyncMessage("content-task:test-result", {
-      id: id,
+      id,
       condition: !err,
       name: err ? err.message : message,
       stack: getStack(err ? err.stack : stack)
     });
   });
 
+  /* eslint-disable no-unused-vars */
   var ok = Assert.ok.bind(Assert);
   var is = Assert.equal.bind(Assert);
   var isnot = Assert.notEqual.bind(Assert);
 
   function todo(expr, name) {
     sendAsyncMessage("content-task:test-todo", {id, expr, name});
   }
 
   function info(name) {
     sendAsyncMessage("content-task:test-info", {id, name});
   }
+  /* eslint-enable no-unused-vars */
 
   try {
     let runnablestr = `
       (() => {
         return (${source});
-      })();`
+      })();`;
 
+    // eslint-disable-next-line no-eval
     let runnable = eval(runnablestr);
     let iterator = runnable.call(this, msg.data.arg);
     Task.spawn(iterator).then((val) => {
       sendAsyncMessage("content-task:complete", {
-        id: id,
+        id,
         result: val,
       });
     }, (e) => {
       sendAsyncMessage("content-task:complete", {
-        id: id,
+        id,
         error: e.toString(),
       });
     });
   } catch (e) {
     sendAsyncMessage("content-task:complete", {
-      id: id,
+      id,
       error: e.toString(),
     });
   }
 });
--- a/testing/mochitest/BrowserTestUtils/content/content-utils.js
+++ b/testing/mochitest/BrowserTestUtils/content/content-utils.js
@@ -1,24 +1,25 @@
 /* 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/. */
 
+/* eslint-env mozilla/frame-script */
+
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 addEventListener("DOMContentLoaded", function(event) {
   let subframe = event.target != content.document;
   // For error page, internalURL is 'about:neterror?...', and visibleURL
   // is the original URL.
   sendAsyncMessage("browser-test-utils:DOMContentLoadedEvent",
-    {subframe: subframe, internalURL: event.target.documentURI,
+    {subframe, internalURL: event.target.documentURI,
      visibleURL: content.document.location.href});
 }, true);
 
 addEventListener("load", function(event) {
   let subframe = event.target != content.document;
   sendAsyncMessage("browser-test-utils:loadEvent",
-    {subframe: subframe, internalURL: event.target.documentURI,
+    {subframe, internalURL: event.target.documentURI,
      visibleURL: content.document.location.href});
 }, true);
-
--- a/testing/mochitest/ShutdownLeaksCollector.jsm
+++ b/testing/mochitest/ShutdownLeaksCollector.jsm
@@ -8,27 +8,27 @@ ChromeUtils.import("resource://gre/modul
 var EXPORTED_SYMBOLS = ["ContentCollector"];
 
 // This listens for the message "browser-test:collect-request". When it gets it,
 // it runs some GCs and CCs, then prints out a message indicating the collections
 // are complete. Mochitest uses this information to determine when windows and
 // docshells should be destroyed.
 
 var ContentCollector = {
-  init: function() {
-      let processType = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType;
+  init() {
+      let processType = Services.appinfo.processType;
       if (processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
         // In the main process, we handle triggering collections in browser-test.js
         return;
       }
 
     Services.cpmm.addMessageListener("browser-test:collect-request", this);
   },
 
-  receiveMessage: function(aMessage) {
+  receiveMessage(aMessage) {
     switch (aMessage.name) {
       case "browser-test:collect-request":
         Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
 
         Cu.forceGC();
         Cu.forceCC();
 
         let shutdownCleanup = aCallback => {
@@ -40,25 +40,25 @@ var ContentCollector = {
             aCallback();
           });
         };
 
         shutdownCleanup(() => {
           setTimeout(() => {
             shutdownCleanup(() => {
               this.finish();
-            })
+            });
           }, 1000);
         });
 
         break;
     }
   },
 
   finish() {
-    let pid = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processID;
+    let pid = Services.appinfo.processID;
     dump("Completed ShutdownLeaks collections in process " + pid + "\n");
 
     Services.cpmm.removeMessageListener("browser-test:collect-request", this);
   },
 
 };
 ContentCollector.init();
--- a/testing/mochitest/bootstrap.js
+++ b/testing/mochitest/bootstrap.js
@@ -8,17 +8,17 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 function loadChromeScripts(win) {
   Services.scriptloader.loadSubScript("chrome://mochikit/content/chrome-harness.js", win);
   Services.scriptloader.loadSubScript("chrome://mochikit/content/mochitest-e10s-utils.js", win);
   Services.scriptloader.loadSubScript("chrome://mochikit/content/browser-test.js", win);
 }
 
-/////// Android ///////
+// ///// Android ///////
 
 Cu.importGlobalProperties(["TextDecoder"]);
 
 const windowTracker = {
   init() {
     Services.obs.addObserver(this, "chrome-document-global-created");
   },
 
@@ -41,51 +41,51 @@ const windowTracker = {
 function androidStartup(data, reason) {
   // Only browser chrome tests need help starting.
   let testRoot = Services.prefs.getStringPref("mochitest.testRoot", "");
   if (testRoot.endsWith("/chrome")) {
     windowTracker.init();
   }
 }
 
-/////// Desktop ///////
+// ///// Desktop ///////
 
 var WindowListener = {
   // browser-test.js is only loaded into the first window. Setup that
   // needs to happen in all navigator:browser windows should go here.
-  setupWindow: function(win) {
+  setupWindow(win) {
     win.nativeConsole = win.console;
     ChromeUtils.defineModuleGetter(win, "console",
       "resource://gre/modules/Console.jsm");
   },
 
-  tearDownWindow: function(win) {
+  tearDownWindow(win) {
     if (win.nativeConsole) {
       win.console = win.nativeConsole;
       win.nativeConsole = undefined;
     }
   },
 
-  onOpenWindow: function (win) {
+  onOpenWindow(win) {
     win = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
 
     win.addEventListener("load", function() {
       if (win.document.documentElement.getAttribute("windowtype") == "navigator:browser") {
         WindowListener.setupWindow(win);
       }
     }, {once: true});
   }
-}
+};
 
 function loadMochitest(e) {
   let flavor = e.detail[0];
   let url = e.detail[1];
 
   let win = Services.wm.getMostRecentWindow("navigator:browser");
-  win.removeEventListener('mochitest-load', loadMochitest);
+  win.removeEventListener("mochitest-load", loadMochitest);
 
   // for mochitest-plain, navigating to the url is all we need
   win.loadURI(url);
   if (flavor == "mochitest") {
     return;
   }
 
   WindowListener.setupWindow(win);
@@ -96,17 +96,17 @@ function loadMochitest(e) {
 
 function startup(data, reason) {
   if (AppConstants.platform == "android") {
     androidStartup(data, reason);
   } else {
     let win = Services.wm.getMostRecentWindow("navigator:browser");
     // wait for event fired from start_desktop.js containing the
     // suite and url to load
-    win.addEventListener('mochitest-load', loadMochitest);
+    win.addEventListener("mochitest-load", loadMochitest);
   }
 }
 
 function shutdown(data, reason) {
   if (AppConstants.platform != "android") {
     let windows = Services.wm.getEnumerator("navigator:browser");
     while (windows.hasMoreElements()) {
       let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -1,61 +1,66 @@
 /* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
+
+/* eslint-env mozilla/browser-window */
+/* import-globals-from chrome-harness.js */
+/* import-globals-from mochitest-e10s-utils.js */
+
 // Test timeout (seconds)
 var gTimeoutSeconds = 45;
 var gConfig;
 var gSaveInstrumentationData = null;
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 ChromeUtils.defineModuleGetter(this, "ContentSearch",
   "resource:///modules/ContentSearch.jsm");
 
 const SIMPLETEST_OVERRIDES =
   ["ok", "is", "isnot", "todo", "todo_is", "todo_isnot", "info", "expectAssertions", "requestCompleteLog"];
 
 // non-android is bootstrapped by marionette
-if (Services.appinfo.OS == 'Android') {
+if (Services.appinfo.OS == "Android") {
   window.addEventListener("load", function() {
     window.addEventListener("MozAfterPaint", function() {
       setTimeout(testInit, 0);
     }, {once: true});
   }, {once: true});
 } else {
   setTimeout(testInit, 0);
 }
 
 var TabDestroyObserver = {
   outstanding: new Set(),
   promiseResolver: null,
 
-  init: function() {
+  init() {
     Services.obs.addObserver(this, "message-manager-close");
     Services.obs.addObserver(this, "message-manager-disconnect");
   },
 
-  destroy: function() {
+  destroy() {
     Services.obs.removeObserver(this, "message-manager-close");
     Services.obs.removeObserver(this, "message-manager-disconnect");
   },
 
-  observe: function(subject, topic, data) {
+  observe(subject, topic, data) {
     if (topic == "message-manager-close") {
       this.outstanding.add(subject);
     } else if (topic == "message-manager-disconnect") {
       this.outstanding.delete(subject);
       if (!this.outstanding.size && this.promiseResolver) {
         this.promiseResolver();
       }
     }
   },
 
-  wait: function() {
+  wait() {
     if (!this.outstanding.size) {
       return Promise.resolve();
     }
 
     return new Promise((resolve) => {
       this.promiseResolver = resolve;
     });
   },
@@ -78,28 +83,32 @@ function testInit() {
                   createInstance(Ci.nsISupportsString);
     sstring.data = location.search;
 
     Services.ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest",
                            "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring);
   } else {
     // This code allows us to redirect without requiring specialpowers for chrome and a11y tests.
     let messageHandler = function(m) {
+      // eslint-disable-next-line no-undef
       messageManager.removeMessageListener("chromeEvent", messageHandler);
       var url = m.json.data;
 
       // Window is the [ChromeWindow] for messageManager, so we need content.window
       // Currently chrome tests are run in a content window instead of a ChromeWindow
+      // eslint-disable-next-line no-undef
       var webNav = content.window.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebNavigation);
       webNav.loadURI(url, null, null, null, null);
     };
 
     var listener = 'data:,function doLoad(e) { var data=e.detail&&e.detail.data;removeEventListener("contentEvent", function (e) { doLoad(e); }, false, true);sendAsyncMessage("chromeEvent", {"data":data}); };addEventListener("contentEvent", function (e) { doLoad(e); }, false, true);';
+    // eslint-disable-next-line no-undef
     messageManager.addMessageListener("chromeEvent", messageHandler);
+    // eslint-disable-next-line no-undef
     messageManager.loadFrameScript(listener, true);
   }
   if (gConfig.e10s) {
     e10s_init();
 
     let processCount = prefs.getIntPref("dom.ipc.processCount", 1);
     if (processCount > 1) {
       // Currently starting a content process is slow, to aviod timeouts, let's
@@ -225,17 +234,17 @@ function takeInstrumentation() {
   function getElementInfo(element) {
     let style = element.ownerGlobal.getComputedStyle(element);
     let binding = style && style.getPropertyValue("-moz-binding");
 
     return {
       namespaceURI: element.namespaceURI,
       localName: element.localName,
       binding: (binding && binding != "none") ? binding : null,
-    }
+    };
   }
 
   // The selector for just this element
   function immediateSelector(element) {
     if (element.localName == "notificationbox" && element.parentNode &&
         element.parentNode.classList.contains("tabbrowser-tabpanels")) {
       // Don't do a full selector for a tabpanel's notificationbox
       return element.localName;
@@ -470,17 +479,17 @@ Tester.prototype = {
   },
   get done() {
     return (this.currentTestIndex == this.tests.length - 1) && (this.repeat <= 0);
   },
 
   start: function Tester_start() {
     TabDestroyObserver.init();
 
-    //if testOnLoad was not called, then gConfig is not defined
+    // if testOnLoad was not called, then gConfig is not defined
     if (!gConfig)
       gConfig = readConfig();
 
     if (gConfig.runUntilFailure)
       this.runUntilFailure = true;
 
     if (gConfig.repeat)
       this.repeat = gConfig.repeat;
@@ -519,17 +528,17 @@ Tester.prototype = {
       }
     }
     // graphics test window is already gone, just call callback immediately
     aCallback();
   },
 
   waitForWindowsState: function Tester_waitForWindowsState(aCallback) {
     let timedOut = this.currentTest && this.currentTest.timedOut;
-    let startTime = Date.now();
+    // eslint-disable-next-line no-nested-ternary
     let baseMsg = timedOut ? "Found a {elt} after previous test timed out"
                            : this.currentTest ? "Found an unexpected {elt} at the end of test run"
                                               : "Found an unexpected {elt}";
 
     // Remove stale tabs
     if (this.currentTest && window.gBrowser && gBrowser.tabs.length > 1) {
       while (gBrowser.tabs.length > 1) {
         let lastTab = gBrowser.tabContainer.lastChild;
@@ -543,17 +552,17 @@ Tester.prototype = {
           }));
         }
         gBrowser.removeTab(lastTab);
       }
     }
 
     // Replace the last tab with a fresh one
     if (window.gBrowser) {
-      let newTab = gBrowser.addTab("about:blank", { skipAnimation: true });
+      gBrowser.addTab("about:blank", { skipAnimation: true });
       gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
       gBrowser.stop();
     }
 
     // Remove stale windows
     this.structuredLogger.info("checking window state");
     let windowsEnum = Services.wm.getEnumerator(null);
     while (windowsEnum.hasMoreElements()) {
@@ -599,17 +608,17 @@ Tester.prototype = {
 
     TabDestroyObserver.destroy();
     Services.console.unregisterListener(this);
 
     // It's important to terminate the module to avoid crashes on shutdown.
     this.PromiseTestUtils.uninit();
 
     // In the main process, we print the ShutdownLeaksCollector message here.
-    let pid = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processID;
+    let pid = Services.appinfo.processID;
     dump("Completed ShutdownLeaks collections in process " + pid + "\n");
 
     this.structuredLogger.info("TEST-START | Shutdown");
 
     if (this.tests.length) {
       let e10sMode = gMultiProcessBrowser ? "e10s" : "non-e10s";
       this.structuredLogger.info("Browser Chrome Test Summary");
       this.structuredLogger.info("Passed:  " + passCount);
@@ -671,18 +680,17 @@ Tester.prototype = {
       let testScope = this.currentTest.scope;
       while (testScope.__cleanupFunctions.length > 0) {
         let func = testScope.__cleanupFunctions.shift();
         try {
           let result = await func.apply(testScope);
           if (isGenerator(result)) {
             this.SimpleTest.ok(false, "Cleanup function returned a generator");
           }
-        }
-        catch (ex) {
+        } catch (ex) {
           this.currentTest.addResult(new testResult({
             name: "Cleanup function threw an exception",
             ex,
             allowFailure: this.currentTest.allowFailure,
           }));
         }
       }
 
@@ -712,17 +720,17 @@ Tester.prototype = {
           allowFailure: this.currentTest.allowFailure,
         }));
       }
 
       this.PromiseTestUtils.ensureDOMPromiseRejectionsProcessed();
       this.PromiseTestUtils.assertNoUncaughtRejections();
       this.PromiseTestUtils.assertNoMoreExpectedRejections();
 
-      Object.keys(window).forEach(function (prop) {
+      Object.keys(window).forEach(function(prop) {
         if (parseInt(prop) == prop) {
           // This is a string which when parsed as an integer and then
           // stringified gives the original string.  As in, this is in fact a
           // string representation of an integer, so an index into
           // window.frames.  Skip those.
           return;
         }
         if (!this._globalProperties.includes(prop)) {
@@ -736,16 +744,17 @@ Tester.prototype = {
         }
       }, this);
 
       // Clear document.popupNode.  The test could have set it to a custom value
       // for its own purposes, nulling it out it will go back to the default
       // behavior of returning the last opened popup.
       document.popupNode = null;
 
+      // eslint-disable-next-line no-undef
       await new Promise(resolve => SpecialPowers.flushPrefEnv(resolve));
 
       if (gConfig.cleanupCrashes) {
         let gdir = Services.dirsvc.get("UAppData", Ci.nsIFile);
         gdir.append("Crash Reports");
         gdir.append("pending");
         if (gdir.exists()) {
           let entries = gdir.directoryEntries;
@@ -832,20 +841,17 @@ Tester.prototype = {
             name: "We expect at least one assertion to fail because this" +
                   " test file is marked as fail-if in the manifest.",
             todo: true,
           }));
         }
       }
 
       // Dump memory stats for main thread.
-      if (Cc["@mozilla.org/xre/runtime;1"]
-          .getService(Ci.nsIXULRuntime)
-          .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
-      {
+      if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
         this.MemoryStats.dump(this.currentTestIndex,
                               this.currentTest.path,
                               gConfig.dumpOutputDirectory,
                               gConfig.dumpAboutMemoryAfterTest,
                               gConfig.dumpDMDAfterTest);
       }
 
       // Note the test run time
@@ -878,17 +884,17 @@ Tester.prototype = {
         if (this._coverageCollector) {
           this._coverageCollector.finalize();
         }
 
         // Uninitialize a few things explicitly so that they can clean up
         // frames and browser intentionally kept alive until shutdown to
         // eliminate false positives.
         if (gConfig.testRoot == "browser") {
-          //Skip if SeaMonkey
+          // Skip if SeaMonkey
           if (AppConstants.MOZ_APP_NAME != "seamonkey") {
             // Replace the document currently loaded in the browser's sidebar.
             // This will prevent false positives for tests that were the last
             // to touch the sidebar. They will thus not be blamed for leaking
             // a document.
             let sidebar = document.getElementById("sidebar");
             sidebar.setAttribute("src", "data:text/html;charset=utf-8,");
             sidebar.docShell.createAboutBlankContentViewer(null);
@@ -1014,17 +1020,17 @@ Tester.prototype = {
       }
     };
 
     // Override SimpleTest methods with ours.
     SIMPLETEST_OVERRIDES.forEach(function(m) {
       this.SimpleTest[m] = this[m];
     }, scope);
 
-    //load the tools to work with chrome .jar and remote
+    // load the tools to work with chrome .jar and remote
     try {
       this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", scope);
     } catch (ex) { /* no chrome-harness tools */ }
 
     // Import head.js script if it exists.
     var currentTestDirPath =
       this.currentTest.path.substr(0, this.currentTest.path.lastIndexOf("/"));
     var headPath = currentTestDirPath + "/head.js";
@@ -1047,17 +1053,16 @@ Tester.prototype = {
       this._scriptLoader.loadSubScript(this.currentTest.path, scope);
       // Run the test
       this.lastStartTime = Date.now();
       if (this.currentTest.scope.__tasks) {
         // This test consists of tasks, added via the `add_task()` API.
         if ("test" in this.currentTest.scope) {
           throw "Cannot run both a add_task test and a normal test at the same time.";
         }
-        let Promise = this.Promise;
         let PromiseTestUtils = this.PromiseTestUtils;
 
         // Allow for a task to be skipped; we need only use the structured logger
         // for this, whilst deactivating log buffering to ensure that messages
         // are always printed to stdout.
         let skipTask = (task) => {
           let logger = this.structuredLogger;
           logger.deactivateBuffering();
@@ -1085,25 +1090,25 @@ Tester.prototype = {
                   name: "Uncaught exception received from previously timed out test",
                   pass: false,
                   ex,
                   stack: (typeof ex == "object" && "stack" in ex) ? ex.stack : null,
                   allowFailure: currentTest.allowFailure,
                 }));
                 // We timed out, so we've already cleaned up for this test, just get outta here.
                 return;
-              } else {
+              }
                 currentTest.addResult(new testResult({
                   name: "Uncaught exception",
                   pass: this.SimpleTest.isExpectingUncaughtException(),
                   ex,
                   stack: (typeof ex == "object" && "stack" in ex) ? ex.stack : null,
                   allowFailure: currentTest.allowFailure,
                 }));
-              }
+
             }
             PromiseTestUtils.assertNoUncaughtRejections();
             this.SimpleTest.info("Leaving test " + task.name);
           }
           this.finish();
         }).call(currentScope);
       } else if (typeof scope.test == "function") {
         scope.test();
@@ -1124,18 +1129,17 @@ Tester.prototype = {
       }
       this.currentTest.scope.finish();
     }
 
     // If the test ran synchronously, move to the next test, otherwise the test
     // will trigger the next test when it is done.
     if (this.currentTest.scope.__done) {
       this.nextTest();
-    }
-    else {
+    } else {
       var self = this;
       var timeoutExpires = Date.now() + gTimeoutSeconds * 1000;
       var waitUntilAtLeast = timeoutExpires - 1000;
       this.currentTest.scope.__waitTimer =
         this.SimpleTest._originalSetTimeout.apply(window, [function timeoutFn() {
         // We sometimes get woken up long before the gTimeoutSeconds
         // have elapsed (when running in chaos mode for example). This
         // code ensures that we don't wrongly time out in that case.
@@ -1173,17 +1177,17 @@ Tester.prototype = {
         self.currentTest.addResult(new testResult({ name: "Test timed out" }));
         self.currentTest.timedOut = true;
         self.currentTest.scope.__waitTimer = null;
         self.nextTest();
       }, gTimeoutSeconds * 1000]);
     }
   },
 
-  QueryInterface: function(aIID) {
+  QueryInterface(aIID) {
     if (aIID.equals(Ci.nsIConsoleListener) ||
         aIID.equals(Ci.nsISupports))
       return this;
 
     throw Cr.NS_ERROR_NO_INTERFACE;
   }
 };
 
@@ -1252,16 +1256,17 @@ function testResult({ name, pass, todo, 
       normalized = "" + stack;
     }
     this.msg += normalized;
   }
 
   if (gConfig.debugOnFailure) {
     // You've hit this line because you requested to break into the
     // debugger upon a testcase failure on your test run.
+    // eslint-disable-next-line no-debugger
     debugger;
   }
 }
 
 function testMessage(msg) {
   this.msg = msg || "";
   this.info = true;
 }
@@ -1305,17 +1310,17 @@ function testScope(aTester, aTest, expec
               Components.stack.caller);
   };
   this.info = function test_info(name) {
     aTest.addResult(new testMessage(name));
   };
 
   this.executeSoon = function test_executeSoon(func) {
     Services.tm.dispatchToMainThread({
-      run: function() {
+      run() {
         func();
       }
     });
   };
 
   this.waitForExplicitFinish = function test_waitForExplicitFinish() {
     self.__done = false;
   };
@@ -1379,30 +1384,31 @@ function testScope(aTester, aTest, expec
       });
     }
   };
 
   this.requestCompleteLog = function test_requestCompleteLog() {
     self.__tester.structuredLogger.deactivateBuffering();
     self.registerCleanupFunction(function() {
       self.__tester.structuredLogger.activateBuffering();
-    })
+    });
   };
 
   // If we're running a test that requires unsafe CPOWs, create a
   // separate sandbox scope, with CPOWS whitelisted, for that test, and
   // mirror all of our properties onto it. Test files will be loaded
   // into this sandbox.
   //
   // Otherwise, load test files directly into the testScope instance.
   if (aTest.usesUnsafeCPOWs) {
     let sandbox = this._createSandbox();
     Cu.permitCPOWsInScope(sandbox);
     return sandbox;
   }
+  return this;
 }
 
 function decorateTaskFn(fn) {
   fn = fn.bind(this);
   fn.skip = () => fn.__skipMe = true;
   fn.only = () => this.__runOnlyThisTask = fn;
   return fn;
 }
@@ -1474,17 +1480,17 @@ testScope.prototype = {
    *   if (!result) {
    *     // Test is ended immediately, with success.
    *     return;
    *   }
    *
    *   is(result, "foo");
    * });
    */
-  add_task: function(aFunction) {
+  add_task(aFunction) {
     if (!this.__tasks) {
       this.waitForExplicitFinish();
       this.__tasks = [];
     }
     let bound = decorateTaskFn.call(this, aFunction);
     this.__tasks.push(bound);
     return bound;
   },
--- a/testing/mochitest/chrome-harness.js
+++ b/testing/mochitest/chrome-harness.js
@@ -1,28 +1,31 @@
 /* -*- 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/. */
 
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 
-ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+/* import-globals-from manifestLibrary.js */
+
+// Defined in browser-test.js
+/* global gTestPath */
 
 /*
  * getChromeURI converts a URL to a URI
  *
  * url: string of a URL (http://mochi.test/test.html)
  * returns: a nsiURI object representing the given URL
  *
  */
 function getChromeURI(url) {
-  var ios = Cc["@mozilla.org/network/io-service;1"].
-              getService(Ci.nsIIOService);
-  return ios.newURI(url);
+  return Services.io.newURI(url);
 }
 
 /*
  * Convert a URL (string) into a nsIURI or NSIJARURI
  * This is intended for URL's that are on a file system
  * or in packaged up in an extension .jar file
  *
  * url: a string of a url on the local system(http://localhost/blah.html)
@@ -30,17 +33,17 @@ function getChromeURI(url) {
 function getResolvedURI(url) {
   var chromeURI = getChromeURI(url);
   var resolvedURI = Cc["@mozilla.org/chrome/chrome-registry;1"].
                     getService(Ci.nsIChromeRegistry).
                     convertChromeURL(chromeURI);
 
   try {
     resolvedURI = resolvedURI.QueryInterface(Ci.nsIJARURI);
-  } catch (ex) {} //not a jar file
+  } catch (ex) {} // not a jar file
 
   return resolvedURI;
 }
 
 /**
  *  getChromeDir is intended to be called after getResolvedURI and convert
  *  the input URI into a nsIFile (actually the directory containing the
  *  file).  This can be used for copying or referencing the file or extra files
@@ -52,34 +55,32 @@ function getResolvedURI(url) {
 function getChromeDir(resolvedURI) {
 
   var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].
                     getService(Ci.nsIFileProtocolHandler);
   var chromeDir = fileHandler.getFileFromURLSpec(resolvedURI.spec);
   return chromeDir.parent.QueryInterface(Ci.nsIFile);
 }
 
-//used by tests to determine their directory based off window.location.path
+// used by tests to determine their directory based off window.location.path
 function getRootDirectory(path, chromeURI) {
-  if (chromeURI === undefined)
-  {
+  if (chromeURI === undefined) {
     chromeURI = getChromeURI(path);
   }
   var myURL = chromeURI.QueryInterface(Ci.nsIURL);
   var mydir = myURL.directory;
 
-  if (mydir.match('/$') != '/')
-  {
-    mydir += '/';
+  if (mydir.match("/$") != "/") {
+    mydir += "/";
   }
 
   return chromeURI.prePath + mydir;
 }
 
-//used by tests to determine their directory based off window.location.path
+// used by tests to determine their directory based off window.location.path
 function getChromePrePath(path, chromeURI) {
 
   if (chromeURI === undefined) {
     chromeURI = getChromeURI(path);
   }
 
   return chromeURI.prePath;
 }
@@ -102,60 +103,58 @@ function getJar(uri) {
  * input:
  *  jar: a nsIJARURI object with the jarfile and jarentry (path in jar file)
  *
  * output;
  *  all files and subdirectories inside jarentry will be extracted to TmpD/mochikit.tmp
  *  we will return the location of /TmpD/mochikit.tmp* so you can reference the files locally
  */
 function extractJarToTmp(jar) {
-  var tmpdir = Cc["@mozilla.org/file/directory_service;1"]
-                      .getService(Ci.nsIProperties)
-                      .get("ProfD", Ci.nsIFile);
+  var tmpdir = Services.dirsvc.get("ProfD", Ci.nsIFile);
   tmpdir.append("mochikit.tmp");
   // parseInt is used because octal escape sequences cause deprecation warnings
   // in strict mode (which is turned on in debug builds)
   tmpdir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8));
 
   var zReader = Cc["@mozilla.org/libjar/zip-reader;1"].
                   createInstance(Ci.nsIZipReader);
 
   var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].
                     getService(Ci.nsIFileProtocolHandler);
 
   var fileName = fileHandler.getFileFromURLSpec(jar.JARFile.spec);
   zReader.open(fileName);
 
-  //filepath represents the path in the jar file without the filename
+  // filepath represents the path in the jar file without the filename
   var filepath = "";
-  var parts = jar.JAREntry.split('/');
-  for (var i =0; i < parts.length - 1; i++) {
-    if (parts[i] != '') {
-      filepath += parts[i] + '/';
+  var parts = jar.JAREntry.split("/");
+  for (var i = 0; i < parts.length - 1; i++) {
+    if (parts[i] != "") {
+      filepath += parts[i] + "/";
     }
   }
 
   /* Create dir structure first, no guarantee about ordering of directories and
    * files returned from findEntries.
    */
-  var dirs = zReader.findEntries(filepath + '*/');
+  var dirs = zReader.findEntries(filepath + "*/");
   while (dirs.hasMore()) {
     var targetDir = buildRelativePath(dirs.getNext(), tmpdir, filepath);
     // parseInt is used because octal escape sequences cause deprecation warnings
     // in strict mode (which is turned on in debug builds)
     if (!targetDir.exists()) {
       targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8));
     }
   }
 
-  //now do the files
+  // now do the files
   var files = zReader.findEntries(filepath + "*");
   while (files.hasMore()) {
     var fname = files.getNext();
-    if (fname.substr(-1) != '/') {
+    if (fname.substr(-1) != "/") {
       var targetFile = buildRelativePath(fname, tmpdir, filepath);
       zReader.extract(fname, targetFile);
     }
   }
   return tmpdir;
 }
 
 /*
@@ -178,74 +177,71 @@ function getTestFilePath(path) {
   } else {
     // Otherwise, we can directly cast it to a file URI
     var fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].
                       getService(Ci.nsIFileProtocolHandler);
     file = fileHandler.getFileFromURLSpec(parentURI.spec);
   }
   // Then walk by the given relative path
   path.split("/")
-      .forEach(function (p) {
+      .forEach(function(p) {
         if (p == "..") {
           file = file.parent;
         } else if (p != ".") {
           file.append(p);
         }
       });
   return file.path;
 }
 
 /*
  * Simple utility function to take the directory structure in jarentryname and
  * translate that to a path of a nsIFile.
  */
-function buildRelativePath(jarentryname, destdir, basepath)
-{
-  var baseParts = basepath.split('/');
-  if (baseParts[baseParts.length-1] == '') {
+function buildRelativePath(jarentryname, destdir, basepath) {
+  var baseParts = basepath.split("/");
+  if (baseParts[baseParts.length - 1] == "") {
     baseParts.pop();
   }
 
-  var parts = jarentryname.split('/');
+  var parts = jarentryname.split("/");
 
   var targetFile = Cc["@mozilla.org/file/local;1"]
                    .createInstance(Ci.nsIFile);
   targetFile.initWithFile(destdir);
 
   for (var i = baseParts.length; i < parts.length; i++) {
     targetFile.append(parts[i]);
   }
 
   return targetFile;
 }
 
 function readConfig(filename) {
   filename = filename || "testConfig.js";
 
-  var fileLocator = Cc["@mozilla.org/file/directory_service;1"].
-                    getService(Ci.nsIProperties);
-  var configFile = fileLocator.get("ProfD", Ci.nsIFile);
+  var configFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
   configFile.append(filename);
 
   if (!configFile.exists())
     return {};
 
   var fileInStream = Cc["@mozilla.org/network/file-input-stream;1"].
                      createInstance(Ci.nsIFileInputStream);
   fileInStream.init(configFile, -1, 0, 0);
 
   var str = NetUtil.readInputStreamToString(fileInStream, fileInStream.available());
   fileInStream.close();
   return JSON.parse(str);
 }
 
 function getTestList(params, callback) {
-  var baseurl = 'chrome://mochitests/content';
+  var baseurl = "chrome://mochitests/content";
   if (window.parseQueryString) {
-    params = parseQueryString(location.search.substring(1), true);
+    params = window.parseQueryString(location.search.substring(1), true);
   }
   if (!params.baseurl) {
     params.baseurl = baseurl;
   }
 
   var config = readConfig();
   for (var p in params) {
     if (params[p] == 1) {
@@ -253,10 +249,10 @@ function getTestList(params, callback) {
     } else if (params[p] == 0) {
       config[p] = false;
     } else {
       config[p] = params[p];
     }
   }
   params = config;
   getTestManifest("http://mochi.test:8888/" + params.manifestFile, params, callback);
-  return;
+
 }
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/chrome-test"
+  ]
+};
--- a/testing/mochitest/chrome/test_sanityPluginUtils.html
+++ b/testing/mochitest/chrome/test_sanityPluginUtils.html
@@ -14,25 +14,25 @@
 <body onload="starttest()">
 <!-- load the test plugin defined at $(DIST)/bin/plugins/Test.plugin/ -->
 <embed id="plugin1" type="application/x-test" width="200" height="200"></embed>
 <script class="testbody" type="text/javascript">
 info("\nProfile::PluginUtilsLoadTime: " + (loadTime - start) + "\n");
 function starttest() {
   SimpleTest.waitForExplicitFinish();
   var startTime = new Date();
-  //increase the runtime of the test so it is detectible, otherwise we get 0-1ms
-  runtimes = 100;
+  // increase the runtime of the test so it is detectible, otherwise we get 0-1ms
+  let runtimes = 100;
   function runTest(plugin) {
     is(plugin.version, "1.0.0.0", "Make sure version is correct");
     is(plugin.name, "Test Plug-in");
-  };
+  }
   while (runtimes > 0) {
     ok(PluginUtils.withTestPlugin(runTest), "Test plugin should be found");
     --runtimes;
   }
   var endTime = new Date();
-  info("\nProfile::PluginUtilsRunTime: " + (endTime-startTime) + "\n");
+  info("\nProfile::PluginUtilsRunTime: " + (endTime - startTime) + "\n");
   SimpleTest.finish();
-};
+}
 </script>
 </body>
 </html>
--- a/testing/mochitest/chunkifyTests.js
+++ b/testing/mochitest/chunkifyTests.js
@@ -1,20 +1,20 @@
 /* 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/. */
 
 function skipTests(tests, startTestPattern, endTestPattern) {
   var startIndex = 0, endIndex = tests.length - 1;
   for (var i = 0; i < tests.length; ++i) {
     var test_path;
-    if ((tests[i] instanceof Object) && ('test' in tests[i])) {
-      test_path = tests[i]['test']['url'];
-    } else if ((tests[i] instanceof Object) && ('url' in tests[i])) {
-      test_path = tests[i]['url'];
+    if ((tests[i] instanceof Object) && ("test" in tests[i])) {
+      test_path = tests[i].test.url;
+    } else if ((tests[i] instanceof Object) && ("url" in tests[i])) {
+      test_path = tests[i].url;
     } else {
       test_path = tests[i];
     }
     if (startTestPattern && test_path.endsWith(startTestPattern)) {
       startIndex = i;
     }
 
     if (endTestPattern && test_path.endsWith(endTestPattern)) {
--- a/testing/mochitest/manifestLibrary.js
+++ b/testing/mochitest/manifestLibrary.js
@@ -1,67 +1,67 @@
 /* -*- 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/. */
 
 function parseTestManifest(testManifest, params, callback) {
-  var links = {};
-  var paths = [];
+  let links = {};
+  let paths = [];
 
   // Support --test-manifest format for mobile
   if ("runtests" in testManifest || "excludetests" in testManifest) {
     callback(testManifest);
     return;
   }
 
-  // For mochitest-chrome and mochitest-browser-chrome harnesses, we 
+  // For mochitest-chrome and mochitest-browser-chrome harnesses, we
   // define tests as links[testname] = true.
   // For mochitest-plain, we define lists as an array of testnames.
-  for (var obj of testManifest['tests']) {
-    var path = obj['path'];
+  for (let obj of testManifest.tests) {
+    let path = obj.path;
     // Note that obj.disabled may be "". We still want to skip in that case.
     if ("disabled" in obj) {
       dump("TEST-SKIPPED | " + path + " | " + obj.disabled + "\n");
       continue;
     }
-    if (params.testRoot != 'tests' && params.testRoot !== undefined) {
-      name = params.baseurl + '/' + params.testRoot + '/' + path;
-      links[name] = {'test': {'url': name, 'expected': obj['expected'], 'uses-unsafe-cpows': obj['uses-unsafe-cpows']}};
+    if (params.testRoot != "tests" && params.testRoot !== undefined) {
+      let name = params.baseurl + "/" + params.testRoot + "/" + path;
+      links[name] = {"test": {"url": name, "expected": obj.expected, "uses-unsafe-cpows": obj["uses-unsafe-cpows"]}};
     } else {
-      name = params.testPrefix + path;
-      paths.push({'test': {'url': name, 'expected': obj['expected'], 'uses-unsafe-cpows': obj['uses-unsafe-cpows']}});
+      let name = params.testPrefix + path;
+      paths.push({"test": {"url": name, "expected": obj.expected, "uses-unsafe-cpows": obj["uses-unsafe-cpows"]}});
     }
   }
   if (paths.length > 0) {
     callback(paths);
   } else {
     callback(links);
   }
 }
 
 function getTestManifest(url, params, callback) {
-  var req = new XMLHttpRequest();
+  let req = new XMLHttpRequest();
   req.open("GET", url);
   req.onload = function() {
     if (req.readyState == 4) {
       if (req.status == 200) {
         try {
           parseTestManifest(JSON.parse(req.responseText), params, callback);
         } catch (e) {
           dump("TEST-UNEXPECTED-FAIL: manifestLibrary.js | error parsing " + url + " (" + e + ")\n");
           throw e;
         }
       } else {
         dump("TEST-UNEXPECTED-FAIL: manifestLibrary.js | error loading " + url + " (HTTP " + req.status + ")\n");
         callback({});
       }
     }
-  }
+  };
   req.send();
 }
 
 // Test Filtering Code
 // TODO Only used by ipc tests, remove once those are implemented sanely
 
 /*
  Open the file referenced by runOnly|exclude and use that to compare against
@@ -69,55 +69,55 @@ function getTestManifest(url, params, ca
  parameters:
    filter = json object of runtests | excludetests
    testList = array of test names to run
    runOnly = use runtests vs excludetests in case both are defined
  returns:
    filtered version of testList
 */
 function filterTests(filter, testList, runOnly) {
-
-  var filteredTests = [];
-  var removedTests = [];
-  var runtests = {};
-  var excludetests = {};
+  let filteredTests = [];
+  let runtests = {};
+  let excludetests = {};
 
   if (filter == null) {
     return testList;
   }
 
-  if ('runtests' in filter) {
+  if ("runtests" in filter) {
     runtests = filter.runtests;
   }
-  if ('excludetests' in filter) {
+  if ("excludetests" in filter) {
     excludetests = filter.excludetests;
   }
-  if (!('runtests' in filter) && !('excludetests' in filter)) {
-    if (runOnly == 'true') {
+  if (!("runtests" in filter) && !("excludetests" in filter)) {
+    if (runOnly == "true") {
       runtests = filter;
     } else {
       excludetests = filter;
     }
   }
 
-  var testRoot = config.testRoot || "tests";
+  // eslint-disable-next-line no-undef
+  let testRoot = config.testRoot || "tests";
   // Start with testList, and put everything that's in 'runtests' in
   // filteredTests.
   if (Object.keys(runtests).length) {
-    for (var i = 0; i < testList.length; i++) {
-      if ((testList[i] instanceof Object) && ('test' in testList[i])) {
-        var testpath = testList[i]['test']['url'];
+    for (let i = 0; i < testList.length; i++) {
+      let testpath;
+      if ((testList[i] instanceof Object) && ("test" in testList[i])) {
+        testpath = testList[i].test.url;
       } else {
-        var testpath = testList[i];
+        testpath = testList[i];
       }
-      var tmppath = testpath.replace(/^\//, '');
-      for (var f in runtests) {
+      let tmppath = testpath.replace(/^\//, "");
+      for (let f in runtests) {
         // Remove leading /tests/ if exists
-        file = f.replace(/^\//, '')
-        file = file.replace(/^tests\//, '')
+        let file = f.replace(/^\//, "");
+        file = file.replace(/^tests\//, "");
 
         // Match directory or filename, testList has <testroot>/<path>
         if (tmppath.match(testRoot + "/" + file) != null) {
           filteredTests.push(testpath);
           break;
         }
       }
     }
@@ -126,29 +126,30 @@ function filterTests(filter, testList, r
   }
 
   // Continue with filteredTests, and deselect everything that's in
   // excludedtests.
   if (!Object.keys(excludetests).length) {
     return filteredTests;
   }
 
-  var refilteredTests = [];
-  for (var i = 0; i < filteredTests.length; i++) {
-    var found = false;
-    if ((filteredTests[i] instanceof Object) && ('test' in filteredTests[i])) {
-      var testpath = filteredTests[i]['test']['url'];
+  let refilteredTests = [];
+  for (let i = 0; i < filteredTests.length; i++) {
+    let found = false;
+    let testpath;
+    if ((filteredTests[i] instanceof Object) && ("test" in filteredTests[i])) {
+      testpath = filteredTests[i].test.url;
     } else {
-      var testpath = filteredTests[i];
+      testpath = filteredTests[i];
     }
-    var tmppath = testpath.replace(/^\//, '');
-    for (var f in excludetests) {
+    let tmppath = testpath.replace(/^\//, "");
+    for (let f in excludetests) {
       // Remove leading /tests/ if exists
-      file = f.replace(/^\//, '')
-      file = file.replace(/^tests\//, '')
+      let file = f.replace(/^\//, "");
+      file = file.replace(/^tests\//, "");
 
       // Match directory or filename, testList has <testroot>/<path>
       if (tmppath.match(testRoot + "/" + file) != null) {
         found = true;
         break;
       }
     }
     if (!found) {
--- a/testing/mochitest/nested_setup.js
+++ b/testing/mochitest/nested_setup.js
@@ -1,31 +1,30 @@
 
-var gTestURL = '';
+/* global SpecialPowers */
 
-function addPermissions()
-{
+var gTestURL = "";
+
+function addPermissions() {
   SpecialPowers.pushPermissions(
     [{ type: "browser", allow: true, context: document }],
     addPreferences);
 }
 
-function addPreferences()
-{
+function addPreferences() {
   SpecialPowers.pushPrefEnv(
     {"set": [["dom.mozBrowserFramesEnabled", true]]},
     insertFrame);
 }
 
-function insertFrame()
-{
+function insertFrame() {
   SpecialPowers.nestedFrameSetup();
 
-  var iframe = document.createElement('iframe');
-  iframe.id = 'nested-parent-frame';
+  var iframe = document.createElement("iframe");
+  iframe.id = "nested-parent-frame";
   iframe.width = "100%";
   iframe.height = "100%";
   iframe.scoring = "no";
   iframe.setAttribute("remote", "true");
   iframe.setAttribute("mozbrowser", "true");
   iframe.src = gTestURL;
   document.getElementById("holder-div").appendChild(iframe);
-}
\ No newline at end of file
+}
--- a/testing/mochitest/redirect.html
+++ b/testing/mochitest/redirect.html
@@ -1,29 +1,27 @@
 <html>
 <head>
   <title>redirecting...</title>
 
   <script type="text/javascript">
-    function redirect(aURL)
-    {
+    function redirect(aURL) {
       // We create a listener for this event in browser-test.js which will
       // get picked up when specifying --flavor=chrome or --flavor=a11y
       var event = new CustomEvent("contentEvent", {
         bubbles: true,
         detail: {
           "data": aURL + location.search,
           "type": "loadURI"
         }
       });
       document.dispatchEvent(event);
     }
 
-    function redirectToHarness()
-    {
+    function redirectToHarness() {
       redirect("chrome://mochikit/content/harness.xul");
     }
 
     function onLoad() {
       // Wait for MozAfterPaint, since the listener in browser-test.js is not
       // added until then.
       window.addEventListener("MozAfterPaint", function() {
         setTimeout(redirectToHarness, 0);
--- a/testing/mochitest/server.js
+++ b/testing/mochitest/server.js
@@ -1,109 +1,122 @@
 /* -*- 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/. */
 
-// Note that the server script itself already defines Cc, Ci, and Cr for us,
-// and because they're constants it's not safe to redefine them.  Scope leakage
-// sucks.
+// We expect these to be defined in the global scope by runtest.py.
+/* global __LOCATION__, _PROFILE_PATH, _SERVER_PORT, _SERVER_ADDR, _DISPLAY_RESULTS,
+          _TEST_PREFIX */
+// Defined by xpcshell
+/* global quit */
+
+/* import-globals-from ../../netwerk/test/httpserver/httpd.js */
 
 // Disable automatic network detection, so tests work correctly when
 // not connected to a network.
+// eslint-disable-next-line mozilla/use-services
 var ios = Cc["@mozilla.org/network/io-service;1"]
             .getService(Ci.nsIIOService);
 ios.manageOfflineStatus = false;
 ios.offline = false;
 
 var server; // for use in the shutdown handler, if necessary
 
 //
 // HTML GENERATION
 //
-var tags = ['A', 'ABBR', 'ACRONYM', 'ADDRESS', 'APPLET', 'AREA', 'B', 'BASE',
-            'BASEFONT', 'BDO', 'BIG', 'BLOCKQUOTE', 'BODY', 'BR', 'BUTTON',
-            'CAPTION', 'CENTER', 'CITE', 'CODE', 'COL', 'COLGROUP', 'DD',
-            'DEL', 'DFN', 'DIR', 'DIV', 'DL', 'DT', 'EM', 'FIELDSET', 'FONT',
-            'FORM', 'FRAME', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
-            'HEAD', 'HR', 'HTML', 'I', 'IFRAME', 'IMG', 'INPUT', 'INS',
-            'ISINDEX', 'KBD', 'LABEL', 'LEGEND', 'LI', 'LINK', 'MAP', 'MENU',
-            'META', 'NOFRAMES', 'NOSCRIPT', 'OBJECT', 'OL', 'OPTGROUP',
-            'OPTION', 'P', 'PARAM', 'PRE', 'Q', 'S', 'SAMP', 'SCRIPT',
-            'SELECT', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'STYLE', 'SUB',
-            'SUP', 'TABLE', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD',
-            'TITLE', 'TR', 'TT', 'U', 'UL', 'VAR'];
+/* global A, ABBR, ACRONYM, ADDRESS, APPLET, AREA, B, BASE,
+          BASEFONT, BDO, BIG, BLOCKQUOTE, BODY, BR, BUTTON,
+          CAPTION, CENTER, CITE, CODE, COL, COLGROUP, DD,
+          DEL, DFN, DIR, DIV, DL, DT, EM, FIELDSET, FONT,
+          FORM, FRAME, FRAMESET, H1, H2, H3, H4, H5, H6,
+          HEAD, HR, HTML, I, IFRAME, IMG, INPUT, INS,
+          ISINDEX, KBD, LABEL, LEGEND, LI, LINK, MAP, MENU,
+          META, NOFRAMES, NOSCRIPT, OBJECT, OL, OPTGROUP,
+          OPTION, P, PARAM, PRE, Q, S, SAMP, SCRIPT,
+          SELECT, SMALL, SPAN, STRIKE, STRONG, STYLE, SUB,
+          SUP, TABLE, TBODY, TD, TEXTAREA, TFOOT, TH, THEAD,
+          TITLE, TR, TT, U, UL, VAR */
+var tags = ["A", "ABBR", "ACRONYM", "ADDRESS", "APPLET", "AREA", "B", "BASE",
+            "BASEFONT", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BR", "BUTTON",
+            "CAPTION", "CENTER", "CITE", "CODE", "COL", "COLGROUP", "DD",
+            "DEL", "DFN", "DIR", "DIV", "DL", "DT", "EM", "FIELDSET", "FONT",
+            "FORM", "FRAME", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6",
+            "HEAD", "HR", "HTML", "I", "IFRAME", "IMG", "INPUT", "INS",
+            "ISINDEX", "KBD", "LABEL", "LEGEND", "LI", "LINK", "MAP", "MENU",
+            "META", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP",
+            "OPTION", "P", "PARAM", "PRE", "Q", "S", "SAMP", "SCRIPT",
+            "SELECT", "SMALL", "SPAN", "STRIKE", "STRONG", "STYLE", "SUB",
+            "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD",
+            "TITLE", "TR", "TT", "U", "UL", "VAR"];
 
 /**
  * Below, we'll use makeTagFunc to create a function for each of the
  * strings in 'tags'. This will allow us to use s-expression like syntax
  * to create HTML.
  */
-function makeTagFunc(tagName)
-{
-  return function (attrs /* rest... */)
-  {
+function makeTagFunc(tagName) {
+  return function(attrs /* rest... */) {
     var startChildren = 0;
     var response = "";
 
     // write the start tag and attributes
     response += "<" + tagName;
     // if attr is an object, write attributes
-    if (attrs && typeof attrs == 'object') {
+    if (attrs && typeof attrs == "object") {
       startChildren = 1;
 
       for (let key in attrs) {
         const value = attrs[key];
         var val = "" + value;
-        response += " " + key + '="' + val.replace('"','&quot;') + '"';
+        response += " " + key + '="' + val.replace('"', "&quot;") + '"';
       }
     }
     response += ">";
 
     // iterate through the rest of the args
     for (var i = startChildren; i < arguments.length; i++) {
-      if (typeof arguments[i] == 'function') {
+      if (typeof arguments[i] == "function") {
         response += arguments[i]();
       } else {
         response += arguments[i];
       }
     }
 
     // write the close tag
     response += "</" + tagName + ">\n";
     return response;
-  }
+  };
 }
 
 function makeTags() {
   // map our global HTML generation functions
   for (let tag of tags) {
       this[tag] = makeTagFunc(tag.toLowerCase());
   }
 }
 
 var _quitting = false;
 
 /** Quit when all activity has completed. */
-function serverStopped()
-{
+function serverStopped() {
   _quitting = true;
 }
 
 // only run the "main" section if httpd.js was loaded ahead of us
-if (this["nsHttpServer"]) {
+if (this.nsHttpServer) {
   //
   // SCRIPT CODE
   //
   runServer();
 
   // We can only have gotten here if the /server/shutdown path was requested.
-  if (_quitting)
-  {
+  if (_quitting) {
     dumpn("HTTP server stopped, all pending requests complete");
     quit(0);
   }
 
   // Impossible as the stop callback should have been called, but to be safe...
   dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server");
   quit(1);
 }
@@ -112,31 +125,30 @@ var serverBasePath;
 var displayResults = true;
 
 var gServerAddress;
 var SERVER_PORT;
 
 //
 // SERVER SETUP
 //
-function runServer()
-{
+function runServer() {
   serverBasePath = __LOCATION__.parent;
   server = createMochitestServer(serverBasePath);
 
-  //verify server address
-  //if a.b.c.d or 'localhost'
+  // verify server address
+  // if a.b.c.d or 'localhost'
   if (typeof(_SERVER_ADDR) != "undefined") {
     if (_SERVER_ADDR == "localhost") {
       gServerAddress = _SERVER_ADDR;
     } else {
-      var quads = _SERVER_ADDR.split('.');
+      var quads = _SERVER_ADDR.split(".");
       if (quads.length == 4) {
         var invalid = false;
-        for (var i=0; i < 4; i++) {
+        for (var i = 0; i < 4; i++) {
           if (quads[i] < 0 || quads[i] > 255)
             invalid = true;
         }
         if (!invalid)
           gServerAddress = _SERVER_ADDR;
         else
           throw new Error("invalid _SERVER_ADDR, please specify a valid IP Address");
       }
@@ -201,18 +213,17 @@ function runServer()
   // and return.
 
   // get rid of any pending requests
   while (thread.hasPendingEvents())
     thread.processNextEvent(true);
 }
 
 /** Creates and returns an HTTP server configured to serve Mochitests. */
-function createMochitestServer(serverBasePath)
-{
+function createMochitestServer(serverBasePath) {
   var server = new nsHttpServer();
 
   server.registerDirectory("/", serverBasePath);
   server.registerPathHandler("/server/shutdown", serverShutdown);
   server.registerPathHandler("/server/debug", serverDebug);
   server.registerPathHandler("/nested_oop", nestedTest);
   server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
   server.registerContentType("jar", "application/x-jar");
@@ -224,41 +235,39 @@ function createMochitestServer(serverBas
   server.registerContentType("dat", "text/plain; charset=utf-8");
   server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader
   server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader
   server.registerContentType("wasm", "application/wasm");
   server.setIndexHandler(defaultDirHandler);
 
   var serverRoot =
     {
-      getFile: function getFile(path)
-      {
+      getFile: function getFile(path) {
         var file = serverBasePath.clone().QueryInterface(Ci.nsIFile);
         path.split("/").forEach(function(p) {
           file.appendRelativePath(p);
         });
         return file;
       },
-      QueryInterface: function(aIID) { return this; }
+      QueryInterface(aIID) { return this; }
     };
 
   server.setObjectState("SERVER_ROOT", serverRoot);
 
   processLocations(server);
 
   return server;
 }
 
 /**
  * Notifies the HTTP server about all the locations at which it might receive
  * requests, so that it can properly respond to requests on any of the hosts it
  * serves.
  */
-function processLocations(server)
-{
+function processLocations(server) {
   var serverLocations = serverBasePath.clone();
   serverLocations.append("server-locations.txt");
 
   const PR_RDONLY = 0x01;
   var fis = new FileInputStream(serverLocations, PR_RDONLY, 292 /* 0444 */,
                                 Ci.nsIFileInputStream.CLOSE_ON_EOF);
 
   var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
@@ -278,36 +287,32 @@ function processLocations(server)
                "(?:" +
                "\\s+" +
                "(\\S+(?:,\\S+)*)" +
                ")?$");
 
   var line = {};
   var lineno = 0;
   var seenPrimary = false;
-  do
-  {
+  do {
     var more = lis.readLine(line);
     lineno++;
 
     var lineValue = line.value;
     if (lineValue.charAt(0) == "#" || lineValue == "")
       continue;
 
     var match = LINE_REGEXP.exec(lineValue);
     if (!match)
       throw new Error("Syntax error in server-locations.txt, line " + lineno);
 
     var [, scheme, host, port, options] = match;
-    if (options)
-    {
-      if (options.split(",").includes("primary"))
-      {
-        if (seenPrimary)
-        {
+    if (options) {
+      if (options.split(",").includes("primary")) {
+        if (seenPrimary) {
           throw new Error("Multiple primary locations in server-locations.txt, " +
                           "line " + lineno);
         }
 
         server.identity.setPrimary(scheme, host, port);
         seenPrimary = true;
         continue;
       }
@@ -316,31 +321,29 @@ function processLocations(server)
     server.identity.add(scheme, host, port);
   }
   while (more);
 }
 
 // PATH HANDLERS
 
 // /server/shutdown
-function serverShutdown(metadata, response)
-{
+function serverShutdown(metadata, response) {
   response.setStatusLine("1.1", 200, "OK");
   response.setHeader("Content-type", "text/plain", false);
 
   var body = "Server shut down.";
   response.bodyOutputStream.write(body, body.length);
 
   dumpn("Server shutting down now...");
   server.stop(serverStopped);
 }
 
 // /server/debug?[012]
-function serverDebug(metadata, response)
-{
+function serverDebug(metadata, response) {
   response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level");
   if (metadata.queryString.length !== 1)
     return;
 
   var mode;
   if (metadata.queryString === "0") {
     // do this now so it gets logged with the old mode
     dumpn("Server debug logs disabled.");
@@ -369,31 +372,29 @@ function serverDebug(metadata, response)
 //
 // DIRECTORY LISTINGS
 //
 
 /**
  * Creates a generator that iterates over the contents of
  * an nsIFile directory.
  */
-function* dirIter(dir)
-{
+function* dirIter(dir) {
   var en = dir.directoryEntries;
   while (en.hasMoreElements()) {
     var file = en.getNext();
     yield file.QueryInterface(Ci.nsIFile);
   }
 }
 
 /**
  * Builds an optionally nested object containing links to the
  * files and directories within dir.
  */
-function list(requestPath, directory, recurse)
-{
+function list(requestPath, directory, recurse) {
   var count = 0;
   var path = requestPath;
   if (path.charAt(path.length - 1) != "/") {
     path += "/";
   }
 
   var dir = directory.QueryInterface(Ci.nsIFile);
   var links = {};
@@ -422,54 +423,50 @@ function list(requestPath, directory, re
     var key = path + file.leafName;
     var childCount = 0;
     if (file.isDirectory()) {
       key += "/";
     }
     if (recurse && file.isDirectory()) {
       [links[key], childCount] = list(key, file, recurse);
       count += childCount;
-    } else {
-      if (file.leafName.charAt(0) != '.') {
-        links[key] = {'test': {'url': key, 'expected': 'pass'}};
+    } else if (file.leafName.charAt(0) != ".") {
+        links[key] = {"test": {"url": key, "expected": "pass"}};
       }
-    }
   }
 
   return [links, count];
 }
 
 /**
  * Heuristic function that determines whether a given path
  * is a test case to be executed in the harness, or just
  * a supporting file.
  */
-function isTest(filename, pattern)
-{
+function isTest(filename, pattern) {
   if (pattern)
     return pattern.test(filename);
 
   // File name is a URL style path to a test file, make sure that we check for
   // tests that start with the appropriate prefix.
   var testPrefix = typeof(_TEST_PREFIX) == "string" ? _TEST_PREFIX : "test_";
   var testPattern = new RegExp("^" + testPrefix);
 
-  var pathPieces = filename.split('/');
+  var pathPieces = filename.split("/");
 
   return testPattern.test(pathPieces[pathPieces.length - 1]) &&
          !filename.includes(".js") &&
          !filename.includes(".css") &&
          !/\^headers\^$/.test(filename);
 }
 
 /**
  * Transform nested hashtables of paths to nested HTML lists.
  */
-function linksToListItems(links)
-{
+function linksToListItems(links) {
   var response = "";
   var children = "";
   for (let link in links) {
     const value = links[link];
     var classVal = (!isTest(link) && !(value instanceof Object))
       ? "non-test invisible"
       : "test";
     if (value instanceof Object) {
@@ -482,39 +479,38 @@ function linksToListItems(links)
     var bug_num = null;
     if (bug_title != null) {
         bug_num = bug_title[0].match(/\d+/);
     }
 
     if ((bug_title == null) || (bug_num == null)) {
       response += LI({class: classVal}, A({href: link}, link), children);
     } else {
-      var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id="+bug_num;
-      response += LI({class: classVal}, A({href: link}, link), " - ", A({href: bug_url}, "Bug "+bug_num), children);
+      var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num;
+      response += LI({class: classVal}, A({href: link}, link), " - ", A({href: bug_url}, "Bug " + bug_num), children);
     }
 
   }
   return response;
 }
 
 /**
  * Transform nested hashtables of paths to a flat table rows.
  */
-function linksToTableRows(links, recursionLevel)
-{
+function linksToTableRows(links, recursionLevel) {
   var response = "";
   for (let link in links) {
     const value = links[link];
-    var classVal = (!isTest(link) && ((value instanceof Object) && ('test' in value)))
+    var classVal = (!isTest(link) && ((value instanceof Object) && ("test" in value)))
       ? "non-test invisible"
       : "";
 
     var spacer = "padding-left: " + (10 * recursionLevel) + "px";
 
-    if ((value instanceof Object) && !('test' in value)) {
+    if ((value instanceof Object) && !("test" in value)) {
       response += TR({class: "dir", id: "tr-" + link },
                      TD({colspan: "3"}, "&#160;"),
                      TD({style: spacer},
                         A({href: link}, link)));
       response += linksToTableRows(value, recursionLevel + 1);
     } else {
       var bug_title = link.match(/test_bug\S+/);
       var bug_num = null;
@@ -541,43 +537,41 @@ function linksToTableRows(links, recursi
     }
   }
   return response;
 }
 
 function arrayOfTestFiles(linkArray, fileArray, testPattern) {
   for (let link in linkArray) {
     const value = linkArray[link];
-    if ((value instanceof Object) && !('test' in value)) {
+    if ((value instanceof Object) && !("test" in value)) {
       arrayOfTestFiles(value, fileArray, testPattern);
     } else if (isTest(link, testPattern) && (value instanceof Object)) {
-      fileArray.push(value['test'])
+      fileArray.push(value.test);
     }
   }
 }
 /**
  * Produce a flat array of test file paths to be executed in the harness.
  */
-function jsonArrayOfTestFiles(links)
-{
+function jsonArrayOfTestFiles(links) {
   var testFiles = [];
   arrayOfTestFiles(links, testFiles);
-  testFiles = testFiles.map(function(file) { return '"' + file['url'] + '"'; });
+  testFiles = testFiles.map(function(file) { return '"' + file.url + '"'; });
 
   return "[" + testFiles.join(",\n") + "]";
 }
 
 /**
  * Produce a normal directory listing.
  */
-function regularListing(metadata, response)
-{
-  var [links, count] = list(metadata.path,
-                            metadata.getProperty("directory"),
-                            false);
+function regularListing(metadata, response) {
+  var [links] = list(metadata.path,
+                     metadata.getProperty("directory"),
+                     false);
   response.write(
     HTML(
       HEAD(
         TITLE("mochitest index ", metadata.path)
       ),
       BODY(
         BR(),
         A({href: ".."}, "Up a level"),
@@ -586,40 +580,38 @@ function regularListing(metadata, respon
     )
   );
 }
 
 /**
  * Read a manifestFile located at the root of the server's directory and turn
  * it into an object for creating a table of clickable links for each test.
  */
-function convertManifestToTestLinks(root, manifest)
-{
+function convertManifestToTestLinks(root, manifest) {
   ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 
   var manifestFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
   manifestFile.initWithFile(serverBasePath);
   manifestFile.append(manifest);
 
   var manifestStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
   manifestStream.init(manifestFile, -1, 0, 0);
 
   var manifestObj = JSON.parse(NetUtil.readInputStreamToString(manifestStream,
                                                                manifestStream.available()));
   var