Merge mozilla-central to inbound. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Mon, 02 Jul 2018 00:53:43 +0300
changeset 479712 bcb6e298dfaf0165684834717c8ae19db16bfae3
parent 479711 e17f5abb81144da115cfc6cefdff93f610913e8c (current diff)
parent 479690 3cfc350101967376909ad3c729f9779ae0ab7a94 (diff)
child 479713 b1878bf341fcab452c4b63f608b66a4a761951df
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone63.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
toolkit/mozapps/extensions/test/browser/browser_legacy_themes.js
--- a/browser/components/payments/res/containers/address-form.css
+++ b/browser/components/payments/res/containers/address-form.css
@@ -19,21 +19,57 @@ address-form[address-fields] #postal-cod
 address-form[address-fields] #country-container,
 address-form[address-fields]:not([address-fields~='email']) #email-container,
 address-form[address-fields]:not([address-fields~='tel']) #tel-container {
   /* !important is needed because autofillEditForms.js sets
      inline styles on the form fields with display: flex; */
   display: none !important;
 }
 
-address-form .error input,
-address-form .error select,
-address-form .error textarea {
-  border: 1px solid #ce001a;
+label[required] > span:first-of-type::after {
+  /* The asterisk should be localized, bug 1472278 */
+  content: "*";
 }
 
 .error-text:not(:empty) {
+  color: #fff;
+  background-color: #d70022;
   border-radius: 2px;
-  background-color: #ce001a;
-  color: #f9e6e9;
-  padding: .1em .5em;
-  margin-inline-start: .1em;
+  /* The padding-top and padding-bottom are referenced by address-form.js */
+  padding: 5px 12px;
+  position: absolute;
+  z-index: 1;
+  pointer-events: none;
+}
+
+body[dir="ltr"] .error-text {
+  left: 3px;
+}
+
+body[dir="rtl"] .error-text {
+  right: 3px;
 }
+
+:-moz-any(input, textarea, select):focus + .error-text:not(:empty)::before {
+  background-color: #d70022;
+  top: -7px;
+  content: '.';
+  height: 16px;
+  position: absolute;
+  text-indent: -999px;
+  transform: rotate(45deg);
+  white-space: nowrap;
+  width: 16px;
+  z-index: -1
+}
+
+body[dir=ltr] .error-text::before {
+  left: 12px
+}
+
+body[dir=rtl] .error-text::before {
+  right: 12px
+}
+
+:-moz-any(input, textarea, select):not(:focus) + .error-text,
+:-moz-any(input, textarea, select):valid + .error-text {
+  display: none;
+}
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -33,24 +33,26 @@ export default class AddressForm extends
     this.saveButton = document.createElement("button");
     this.saveButton.className = "save-button primary";
     this.saveButton.addEventListener("click", this);
 
     this.persistCheckbox = new LabelledCheckbox();
     this.persistCheckbox.className = "persist-checkbox";
 
     this._errorFieldMap = {
-      addressLine: "#street-address-container",
-      city: "#address-level2-container",
-      country: "#country-container",
-      organization: "#organization-container",
-      phone: "#tel-container",
-      postalCode: "#postal-code-container",
-      recipient: "#name-container",
-      region: "#address-level1-container",
+      addressLine: "#street-address",
+      city: "#address-level2",
+      country: "#country",
+      organization: "#organization",
+      phone: "#tel",
+      postalCode: "#postal-code",
+      // Bug 1472283 is on file to support
+      // additional-name and family-name.
+      recipient: "#given-name",
+      region: "#address-level1",
     };
 
     // 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;
     });
@@ -138,30 +140,69 @@ export default class AddressForm extends
     } else {
       // Adding a new record: default persistence to checked when in a not-private session
       this.persistCheckbox.hidden = false;
       this.persistCheckbox.checked = !state.isPrivate;
     }
 
     this.formHandler.loadRecord(record);
 
+    // Add validation to some address fields
+    for (let formElement of this.form.elements) {
+      let container = formElement.closest(`#${formElement.id}-container`);
+      if (formElement.localName == "button" || !container) {
+        continue;
+      }
+      let required = formElement.required && !formElement.disabled;
+      if (required) {
+        container.setAttribute("required", "true");
+      } else {
+        container.removeAttribute("required");
+      }
+    }
+
     let shippingAddressErrors = request.paymentDetails.shippingAddressErrors;
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
-      let container = document.querySelector(errorSelector);
+      let container = document.querySelector(errorSelector + "-container");
+      let field = document.querySelector(errorSelector);
+      let errorText = (shippingAddressErrors && shippingAddressErrors[errorName]) || "";
+      container.classList.toggle("error", !!errorText);
+      field.setCustomValidity(errorText);
       let span = container.querySelector(".error-text");
       if (!span) {
         span = document.createElement("span");
         span.className = "error-text";
         container.appendChild(span);
       }
-      span.textContent = shippingAddressErrors[errorName];
-      container.classList.toggle("error", !!shippingAddressErrors[errorName]);
+      span.textContent = errorText;
+    }
+
+    // Position the error messages all at once so layout flushes only once.
+    let formRect = this.form.getBoundingClientRect();
+    let errorSpanData = [...this.form.querySelectorAll(".error-text:not(:empty)")].map(span => {
+      let relatedInput = span.previousElementSibling;
+      let relatedRect = relatedInput.getBoundingClientRect();
+      return {
+        span,
+        top: relatedRect.bottom,
+        left: relatedRect.left - formRect.left,
+        right: formRect.right - relatedRect.right,
+      };
+    });
+    let isRTL = this.form.matches(":dir(rtl)");
+    for (let data of errorSpanData) {
+      // Subtract 10px for the padding-top and padding-bottom.
+      data.span.style.top = (data.top - 10) + "px";
+      if (isRTL) {
+        data.span.style.right = data.right + "px";
+      } else {
+        data.span.style.left = data.left + "px";
+      }
     }
   }
-
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
       }
     }
   }
--- a/browser/components/payments/res/unprivileged-fallbacks.js
+++ b/browser/components/payments/res/unprivileged-fallbacks.js
@@ -37,11 +37,14 @@ var PaymentDialogUtils = {
       "fieldsOrder": [
         {fieldId: "name", newLine: true},
         {fieldId: "organization", newLine: true},
         {fieldId: "street-address", newLine: true},
         {fieldId: "address-level2"},
         {fieldId: "address-level1"},
         {fieldId: "postal-code"},
       ],
+      // The following values come from addressReferences.js and should not be changed.
+      /* eslint-disable-next-line max-len */
+      "postalCodePattern": country == "US" ? "(\\d{5})(?:[ \\-](\\d{4}))?" : "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d",
     };
   },
 };
--- a/browser/components/payments/test/browser/browser_card_edit.js
+++ b/browser/components/payments/test/browser/browser_card_edit.js
@@ -348,27 +348,27 @@ add_task(async function test_edit_link()
     }, "Check address page state (editing)");
 
     info("filling address fields");
     for (let [key, val] of Object.entries(PTU.Addresses.TimBL)) {
       let field = content.document.getElementById(key);
       if (!field) {
         ok(false, `${key} field not found`);
       }
-      field.value = val + "1";
+      field.value = val.slice(0, -1) + "7";
       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 state.page.id == "basic-card-page" && state["basic-card-page"].guid &&
              Object.keys(state.savedAddresses).length == 1;
     }, "Check still only one address and we're back on basic-card page");
 
-    is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel + "1",
+    is(Object.values(state.savedAddresses)[0].tel, PTU.Addresses.TimBL.tel.slice(0, -1) + "7",
        "Check that address was edited and saved");
 
     content.document.querySelector("basic-card-form button:last-of-type").click();
 
     state = await PTU.DialogContentUtils.waitForState(content, (state) => {
       let cards = Object.entries(state.savedBasicCards);
       return cards.length == 1 &&
              cards[0][1]["cc-name"] == card["cc-name"];
--- a/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
+++ b/browser/components/payments/test/browser/browser_shippingaddresschange_error.js
@@ -126,17 +126,17 @@ add_task(async function test_show_field_
       // check errors and make corrections
       let {shippingAddressErrors} = PTU.Details.fieldSpecificErrors;
       is(content.document.querySelectorAll("address-form .error-text:not(:empty)").length,
          Object.keys(shippingAddressErrors).length,
          "Each error should be presented");
       let errorFieldMap =
         Cu.waiveXrays(content.document.querySelector("address-form"))._errorFieldMap;
       for (let [errorName, errorValue] of Object.entries(shippingAddressErrors)) {
-        let field = content.document.querySelector(errorFieldMap[errorName]);
+        let field = content.document.querySelector(errorFieldMap[errorName] + "-container");
         try {
           is(field.querySelector(".error-text").textContent, errorValue,
              "Field specific error should be associated with " + errorName);
         } catch (ex) {
           ok(false, `no field found for ${errorName}. selector= ${errorFieldMap[errorName]}`);
         }
       }
     });
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -25,17 +25,17 @@ Test the address-form element
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 <script type="module">
 /** Test the address-form element **/
 
-/* global sinon */
+/* global sinon, PaymentDialogUtils */
 
 import AddressForm from "../../res/containers/address-form.js";
 
 let display = document.getElementById("display");
 
 function checkAddressForm(customEl, expectedAddress) {
   const ADDRESS_PROPERTY_NAMES = [
     "given-name",
@@ -52,16 +52,26 @@ function checkAddressForm(customEl, expe
   for (let propName of ADDRESS_PROPERTY_NAMES) {
     let expectedVal = expectedAddress[propName] || "";
     is(document.getElementById(propName).value,
        expectedVal.toString(),
        `Check ${propName}`);
   }
 }
 
+function sendStringAndCheckValidity(element, string, isValid) {
+  element.focus();
+  while (element.value) {
+    sendKey("BACK_SPACE");
+  }
+  sendString(string);
+  ok(element.checkValidity() == isValid,
+     `${element.id} should be ${isValid ? "valid" : "invalid"} (${string})`);
+}
+
 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");
@@ -270,12 +280,162 @@ add_task(async function test_restricted_
      "country should be hidden");
   ok(!isHidden(form.form.querySelector("#email")),
      "email should be visible");
   ok(!isHidden(form.form.querySelector("#tel")),
      "tel should be visible");
 
   form.remove();
 });
+
+add_task(async function test_field_validation() {
+  let form = new AddressForm();
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let postalCodeInput = form.form.querySelector("#postal-code");
+  let addressLevel1Input = form.form.querySelector("#address-level1");
+  ok(!postalCodeInput.value, "postal-code should be empty by default");
+  ok(!addressLevel1Input.value, "address-level1 should be empty by default");
+  ok(!postalCodeInput.checkValidity(), "postal-code should be invalid by default");
+  ok(!addressLevel1Input.checkValidity(), "address-level1 should be invalid by default");
+
+  let countrySelect = form.form.querySelector("#country");
+  let requiredFields = [
+    form.form.querySelector("#given-name"),
+    form.form.querySelector("#street-address"),
+    form.form.querySelector("#address-level2"),
+    postalCodeInput,
+    addressLevel1Input,
+    countrySelect,
+  ];
+  for (let field of requiredFields) {
+    let container = field.closest("label");
+    ok(container.hasAttribute("required"), "Container should have required attribute");
+    let label = field.closest("label").querySelector("span");
+    is(getComputedStyle(label, "::after").content, "\"*\"", "Asterisk should be on " + field.id);
+  }
+
+  countrySelect.selectedIndex = [...countrySelect.options].findIndex(o => o.value == "US");
+  countrySelect.dispatchEvent(new Event("change"));
+
+  sendStringAndCheckValidity(addressLevel1Input, "MI", true);
+  sendStringAndCheckValidity(addressLevel1Input, "", false);
+  sendStringAndCheckValidity(postalCodeInput, "B4N4N4", false);
+  sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
+  sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", false);
+  sendStringAndCheckValidity(addressLevel1Input, "", false);
+  sendStringAndCheckValidity(postalCodeInput, "11109", true);
+  sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
+  sendStringAndCheckValidity(postalCodeInput, "06390-0001", true);
+
+  countrySelect.selectedIndex = [...countrySelect.options].findIndex(o => o.value == "CA");
+  countrySelect.dispatchEvent(new Event("change"));
+
+  sendStringAndCheckValidity(postalCodeInput, "00001", false);
+  sendStringAndCheckValidity(addressLevel1Input, "CA", true);
+  sendStringAndCheckValidity(postalCodeInput, "94043", false);
+  sendStringAndCheckValidity(addressLevel1Input, "", false);
+  sendStringAndCheckValidity(postalCodeInput, "B4N4N4", true);
+  sendStringAndCheckValidity(addressLevel1Input, "MI", true);
+  sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", true);
+  sendStringAndCheckValidity(addressLevel1Input, "", false);
+  sendStringAndCheckValidity(postalCodeInput, "11109", false);
+  sendStringAndCheckValidity(addressLevel1Input, "Nova Scotia", true);
+  sendStringAndCheckValidity(postalCodeInput, "06390-0001", false);
+
+  form.remove();
+});
+
+add_task(async function test_customValidity() {
+  let form = new AddressForm();
+  await form.promiseReady;
+  const state = {
+    page: {
+      id: "address-page",
+    },
+    "address-page": {
+      title: "Sample page title",
+    },
+    request: {
+      paymentDetails: {
+        shippingAddressErrors: {
+          addressLine: "Street address needs to start with a D",
+          city: "City needs to start with a B",
+          country: "Country needs to start with a C",
+          organization: "organization needs to start with an A",
+          phone: "Telephone needs to start with a 9",
+          postalCode: "Postal code needs to start with a 0",
+          recipient: "Name needs to start with a Z",
+          region: "Region needs to start with a Y",
+        },
+      },
+    },
+  };
+  await form.requestStore.setState(state);
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  function checkValidationMessage(selector, property) {
+    is(form.form.querySelector(selector).validationMessage,
+       state.request.paymentDetails.shippingAddressErrors[property],
+       "Validation message should match for " + selector);
+  }
+
+  checkValidationMessage("#street-address", "addressLine");
+  checkValidationMessage("#address-level2", "city");
+  checkValidationMessage("#country", "country");
+  checkValidationMessage("#organization", "organization");
+  checkValidationMessage("#tel", "phone");
+  checkValidationMessage("#postal-code", "postalCode");
+  checkValidationMessage("#given-name", "recipient");
+  checkValidationMessage("#address-level1", "region");
+
+  form.remove();
+});
+
+add_task(async function test_field_validation() {
+  sinon.stub(PaymentDialogUtils, "getFormFormat").returns({
+    addressLevel1Label: "state",
+    postalCodeLabel: "US",
+    fieldsOrder: [
+      {fieldId: "name", newLine: true},
+      {fieldId: "organization", newLine: true},
+      {fieldId: "street-address", newLine: true},
+      {fieldId: "address-level2"},
+    ],
+  });
+
+  let form = new AddressForm();
+  await form.promiseReady;
+  const state = {
+    page: {
+      id: "address-page",
+    },
+    "address-page": {
+      title: "Sample page title",
+    },
+    request: {
+      paymentDetails: {
+        shippingAddressErrors: {},
+      },
+    },
+  };
+  await form.requestStore.setState(state);
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let postalCodeInput = form.form.querySelector("#postal-code");
+  let addressLevel1Input = form.form.querySelector("#address-level1");
+  ok(!postalCodeInput.value, "postal-code should be empty by default");
+  ok(!addressLevel1Input.value, "address-level1 should be empty by default");
+  ok(postalCodeInput.checkValidity(),
+     "postal-code should be valid by default when it is not visible");
+  ok(addressLevel1Input.checkValidity(),
+     "address-level1 should be valid by default when it is not visible");
+
+  form.remove();
+});
 </script>
 
 </body>
 </html>
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -778,16 +778,17 @@ this.FormAutofillUtils = {
    *         }
    */
   getFormFormat(country) {
     const dataset = this.getCountryAddressData(country);
     return {
       "addressLevel1Label": dataset.state_name_type || "province",
       "postalCodeLabel": dataset.zip_name_type || "postalCode",
       "fieldsOrder": this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C, %S %Z"),
+      "postalCodePattern": dataset.zip,
     };
   },
 
   /**
    * Localize "data-localization" or "data-localization-region" attributes.
    * @param {Element} element
    * @param {string} attributeName
    */
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ b/browser/extensions/formautofill/content/autofillEditForms.js
@@ -91,16 +91,20 @@ class EditAddress extends EditAutofillFo
       country: this._elements.form.querySelector("#country"),
     });
 
     this.populateCountries();
     // Need to populate the countries before trying to set the initial country.
     // Also need to use this._record so it has the default country selected.
     this.loadRecord(record);
     this.attachEventListeners();
+
+    if (config.novalidate) {
+      this.form.setAttribute("novalidate", "true");
+    }
   }
 
   loadRecord(record) {
     this._record = record;
     if (!record) {
       record = {
         country: this.supportedCountries.find(supported => supported == this.DEFAULT_REGION),
       };
@@ -110,50 +114,66 @@ class EditAddress extends EditAutofillFo
   }
 
   /**
    * Format the form based on country. The address-level1 and postal-code labels
    * should be specific to the given country.
    * @param  {string} country
    */
   formatForm(country) {
-    const {addressLevel1Label, postalCodeLabel, fieldsOrder} = this.getFormFormat(country);
+    const {addressLevel1Label, postalCodeLabel, fieldsOrder, postalCodePattern} =
+      this.getFormFormat(country);
     this._elements.addressLevel1Label.dataset.localization = addressLevel1Label;
     this._elements.postalCodeLabel.dataset.localization = postalCodeLabel;
     this.arrangeFields(fieldsOrder);
+    this.updatePostalCodeValidation(postalCodePattern);
   }
 
   arrangeFields(fieldsOrder) {
     let fields = [
       "name",
       "organization",
       "street-address",
       "address-level2",
       "address-level1",
       "postal-code",
     ];
     let inputs = [];
     for (let i = 0; i < fieldsOrder.length; i++) {
       let {fieldId, newLine} = fieldsOrder[i];
       let container = document.getElementById(`${fieldId}-container`);
-      inputs.push(...container.querySelectorAll("input, textarea, select"));
+      let containerInputs = [...container.querySelectorAll("input, textarea, select")];
+      containerInputs.forEach(function(input) { input.disabled = false; });
+      inputs.push(...containerInputs);
       container.style.display = "flex";
       container.style.order = i;
       container.style.pageBreakAfter = newLine ? "always" : "auto";
       // Remove the field from the list of fields
       fields.splice(fields.indexOf(fieldId), 1);
     }
     for (let i = 0; i < inputs.length; i++) {
       // Assign tabIndex starting from 1
       inputs[i].tabIndex = i + 1;
     }
     // Hide the remaining fields
     for (let field of fields) {
       let container = document.getElementById(`${field}-container`);
       container.style.display = "none";
+      for (let input of [...container.querySelectorAll("input, textarea, select")]) {
+        input.disabled = true;
+      }
+    }
+  }
+
+  updatePostalCodeValidation(postalCodePattern) {
+    let postalCodeInput = document.getElementById("postal-code");
+    if (postalCodePattern && postalCodeInput.style.display != "none") {
+      postalCodeInput.setAttribute("pattern", postalCodePattern);
+    } else {
+      postalCodeInput.removeAttribute("pattern");
     }
   }
 
   populateCountries() {
     let fragment = document.createDocumentFragment();
     for (let country of this.supportedCountries) {
       let option = new Option();
       option.value = country;
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -17,51 +17,51 @@
   <script src="chrome://formautofill/content/autofillEditForms.js"></script>
 </head>
 <body dir="&locale.dir;">
   <form id="form" autocomplete="off">
     <div>
       <div id="name-container">
         <label id="given-name-container">
           <span data-localization="givenName"/>
-          <input id="given-name" type="text"/>
+          <input id="given-name" type="text" required="true"/>
         </label>
         <label id="additional-name-container">
           <span data-localization="additionalName"/>
           <input id="additional-name" type="text"/>
         </label>
         <label id="family-name-container">
           <span data-localization="familyName"/>
           <input id="family-name" type="text"/>
         </label>
       </div>
       <label id="organization-container">
         <span data-localization="organization2"/>
         <input id="organization" type="text"/>
       </label>
       <label id="street-address-container">
         <span data-localization="streetAddress"/>
-        <textarea id="street-address" rows="3"/>
+        <textarea id="street-address" rows="3" required="true"/>
       </label>
       <label id="address-level2-container">
         <span data-localization="city"/>
-        <input id="address-level2" type="text"/>
+        <input id="address-level2" type="text" required="true"/>
       </label>
       <label id="address-level1-container">
         <span/>
-        <input id="address-level1" type="text"/>
+        <input id="address-level1" type="text" required="true"/>
       </label>
       <label id="postal-code-container">
         <span/>
-        <input id="postal-code" type="text"/>
+        <input id="postal-code" type="text" required="true"/>
       </label>
     </div>
     <label id="country-container">
       <span data-localization="country"/>
-      <select id="country">
+      <select id="country" required="true">
         <option/>
       </select>
     </label>
     <p id="country-warning-message" data-localization="countryWarningMessage2"/>
     <label id="email-container">
       <span data-localization="email"/>
       <input id="email" type="email"/>
     </label>
@@ -78,24 +78,26 @@
     "use strict";
 
     let {
       DEFAULT_REGION,
       getFormFormat,
       supportedCountries,
     } = FormAutofillUtils;
     let record = window.arguments && window.arguments[0];
+    let novalidate = window.arguments && window.arguments[1] == "novalidate";
 
     /* import-globals-from autofillEditForms.js */
     let fieldContainer = new EditAddress({
       form: document.getElementById("form"),
     }, record, {
       DEFAULT_REGION,
       getFormFormat: getFormFormat.bind(FormAutofillUtils),
       supportedCountries,
+      novalidate,
     });
 
     /* import-globals-from editDialog.js */
     new EditAddressDialog({
       title: document.querySelector("title"),
       fieldContainer,
       controlsContainer: document.getElementById("controls-container"),
       cancel: document.getElementById("cancel"),
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.js
@@ -287,17 +287,17 @@ class ManageAddresses extends ManageReco
   }
 
   /**
    * Open the edit address dialog to create/edit an address.
    *
    * @param  {object} address [optional]
    */
   openEditDialog(address) {
-    this.prefWin.gSubDialog.open(EDIT_ADDRESS_URL, null, address);
+    this.prefWin.gSubDialog.open(EDIT_ADDRESS_URL, null, address, "novalidate");
   }
 
   getLabel(address) {
     return FormAutofillUtils.getAddressLabel(address);
   }
 }
 
 class ManageCreditCards extends ManageRecords {
--- a/toolkit/content/aboutSupport.js
+++ b/toolkit/content/aboutSupport.js
@@ -89,17 +89,32 @@ var snapshotFormatters = {
         case Services.policies.ACTIVE:
           policiesText = strings.GetStringFromName("policies.active");
           break;
 
         default:
           policiesText = strings.GetStringFromName("policies.error");
           break;
       }
-      $("policies-status").textContent = policiesText;
+
+      if (data.policiesStatus == Services.policies.ACTIVE) {
+        let activePolicies = $.new("a", policiesText);
+        activePolicies.addEventListener("click", function(event) {
+          let activePoliciesJson = {};
+          activePoliciesJson.policies = Services.policies.getActivePolicies();
+          let activePoliciesJsonBlob = new Blob([JSON.stringify(activePoliciesJson)],
+                                                {type: "application/json"});
+          let jsonURL = URL.createObjectURL(activePoliciesJsonBlob);
+          window.open(jsonURL);
+          URL.revokeObjectURL(jsonURL);
+        });
+        $("policies-status").appendChild(activePolicies);
+      } else {
+        $("policies-status").textContent = policiesText;
+      }
     } else {
       $("policies-status-row").hidden = true;
     }
 
     let keyGoogleFound = data.keyGoogleFound ? "found" : "missing";
     $("key-google-box").textContent = strings.GetStringFromName(keyGoogleFound);
 
     let keyMozillaFound = data.keyMozillaFound ? "found" : "missing";
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -730,26 +730,26 @@ AddonWrapper.prototype = {
  *          The ID to be converted
  *
  * @return  the lightweight theme ID or null if the ID was not for a lightweight
  *          theme.
  */
 function _getInternalID(id) {
   if (!id)
     return null;
-  if (id == DEFAULT_THEME_ID)
+  if (LightweightThemeManager._builtInThemes.has(id))
     return id;
   let len = id.length - ID_SUFFIX.length;
   if (len > 0 && id.substring(len) == ID_SUFFIX)
     return id.substring(0, len);
   return null;
 }
 
 function _getExternalID(id) {
-  if (id == DEFAULT_THEME_ID)
+  if (LightweightThemeManager._builtInThemes.has(id))
     return id;
   return id + ID_SUFFIX;
 }
 
 function _setCurrentTheme(aData, aLocal) {
   aData = _sanitizeTheme(aData, null, aLocal);
 
   let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -235,23 +235,17 @@ function isDisabledUnsigned(addon) {
 }
 
 function isLegacyExtension(addon) {
   let legacy = false;
   if (addon.type == "extension" && !addon.isWebExtension) {
     legacy = true;
   }
   if (addon.type == "theme") {
-    // The logic here is kind of clunky but we want to mark complete
-    // themes as legacy.  There's no explicit flag for complete
-    // themes so explicitly check for new style themes (for which
-    // isWebExtension is true) or lightweight themes (which have
-    // ids that end with @personas.mozilla.org)
-    legacy = !(addon.isWebExtension || addon.id.endsWith("@personas.mozilla.org") ||
-               addon.id == "default-theme@mozilla.org");
+    legacy = false;
   }
 
   if (legacy && (addon.hidden || addon.signedState == AddonManager.SIGNEDSTATE_PRIVILEGED)) {
     legacy = false;
   }
   // Exceptions that can slip through above: the default theme plus
   // test pilot addons until we get SIGNEDSTATE_PRIVILEGED deployed.
   if (legacy && legacyWarningExceptions.includes(addon.id)) {
@@ -2578,37 +2572,17 @@ var gDetailView = {
     this._addon = aAddon;
     gEventManager.registerAddonListener(this, aAddon.id);
     gEventManager.registerInstallListener(this);
 
     this.node.setAttribute("type", aAddon.type);
 
     let legacy = false;
     if (!aAddon.install) {
-      if (aAddon.type == "extension" && !aAddon.isWebExtension) {
-        legacy = true;
-      }
-      if (aAddon.type == "theme") {
-        // The logic here is kind of clunky but we want to mark complete
-        // themes as legacy.  There's no explicit flag for complete
-        // themes so explicitly check for new style themes (for which
-        // isWebExtension is true) or lightweight themes (which have
-        // ids that end with @personas.mozilla.org)
-        legacy = !(aAddon.isWebExtension || aAddon.id.endsWith("@personas.mozilla.org"));
-      }
-
-      if (legacy && aAddon.signedState == AddonManager.SIGNEDSTATE_PRIVILEGED) {
-        legacy = false;
-      }
-
-      // Exceptions that can slip through above: the default theme plus
-      // test pilot addons until we get SIGNEDSTATE_PRIVILEGED deployed.
-      if (legacy && legacyWarningExceptions.includes(aAddon.id)) {
-        legacy = false;
-      }
+      legacy = isLegacyExtension(aAddon);
     }
     this.node.setAttribute("legacy", legacy);
     document.getElementById("detail-legacy-warning").href = SUPPORT_URL + "webextensions";
 
     // Make sure to select the correct category
     let category = (isDisabledLegacy(aAddon) || isDisabledUnsigned(aAddon)) ?
                    "addons://legacy" : `addons://list/${aAddon.type}`;
     gCategories.select(category);
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -65,17 +65,16 @@ skip-if = true # Bug 1449071 - Frequent 
 skip-if = os == 'linux' && !debug # Bug 1398766
 [browser_inlinesettings_browser.js]
 skip-if = (verify && debug && (os == 'mac'))
 [browser_installssl.js]
 skip-if = verify
 [browser_langpack_signing.js]
 [browser_legacy.js]
 [browser_legacy_pre57.js]
-[browser_legacy_themes.js]
 [browser_list.js]
 [browser_manualupdates.js]
 [browser_pluginprefs.js]
 [browser_pluginprefs_is_not_disabled.js]
 [browser_plugin_enabled_state_locked.js]
 [browser_recentupdates.js]
 [browser_sorting.js]
 [browser_sorting_plugins.js]
--- a/toolkit/mozapps/extensions/test/browser/browser_legacy_pre57.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_legacy_pre57.js
@@ -1,28 +1,21 @@
 
 add_task(async function() {
   const INFO_URL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "webextensions";
 
   const NAMES = {
-    fullTheme: "Full Theme",
     newTheme: "New LWT",
     legacy: "Legacy Extension",
     webextension: "WebExtension",
     dictionary: "Dictionary",
     langpack: "Language Pack",
   };
   let addons = [
     {
-      id: "full-theme@tests.mozilla.org",
-      name: NAMES.fullTheme,
-      type: "theme",
-      isWebExtension: false,
-    },
-    {
       id: "new-theme@tests.mozilla.org",
       name: NAMES.newTheme,
       type: "theme",
       isWebExtension: true,
     },
     {
       id: "legacy@tests.mozilla.org",
       name: NAMES.legacy,
@@ -82,16 +75,15 @@ add_task(async function() {
     if (isLegacy) {
       is_element_visible(badge, `Legacy badge is visible for ${name}`);
       is(badge.href, INFO_URL, "Legacy badge link is correct");
     } else {
       is_element_hidden(badge, `Legacy badge is hidden for ${name}`);
     }
   }
 
-  await check("theme", NAMES.fullTheme, true);
   await check("theme", NAMES.newTheme, false);
   await check("extension", NAMES.legacy, true);
   await check("extension", NAMES.webextension, false);
   await check("dictionary", NAMES.dictionary, false);
 
   await close_manager(mgrWin);
 });
deleted file mode 100644
--- a/toolkit/mozapps/extensions/test/browser/browser_legacy_themes.js
+++ /dev/null
@@ -1,77 +0,0 @@
-
-add_task(async function() {
-  // The mochitest framework installs a bunch of legacy extensions.
-  // Fortunately, the extensions.legacy.exceptions preference exists to
-  // avoid treating some extensions as legacy for the purposes of the UI.
-  const IGNORE = [
-    "special-powers@mozilla.org",
-    "mochikit@mozilla.org",
-  ];
-
-  let exceptions = Services.prefs.getCharPref("extensions.legacy.exceptions");
-  exceptions = [ exceptions, ...IGNORE ].join(",");
-
-  await SpecialPowers.pushPrefEnv({
-    set: [
-      ["extensions.legacy.enabled", false],
-      ["extensions.legacy.exceptions", exceptions],
-    ],
-  });
-
-  const ID = "theme@tests.mozilla.org";
-
-  let provider = new MockProvider();
-  provider.createAddons([{
-    id: ID,
-    name: "Complete Theme",
-    type: "theme",
-    appDisabled: true,
-  }]);
-
-  // Open about:addons and go to the themes list
-  let mgrWin = await open_manager(null);
-  let catUtils = new CategoryUtilities(mgrWin);
-  await catUtils.openType("theme");
-
-  // Our complete theme should not be displayed
-  let list = mgrWin.document.getElementById("addon-list");
-  let item = list.children.find(item => item.mAddon.id == ID);
-  is(item, undefined, `Theme ${ID} should not be in the list of active themes`);
-
-  // The warning banner and the legacy category should both be visible
-  let banner = mgrWin.document.getElementById("legacy-extensions-notice");
-  is_element_visible(banner, "Warning about legacy themes should be visible");
-  is(mgrWin.gLegacyView._categoryItem.disabled, false, "Legacy category should be visible ");
-
-  // Follow the link to the legacy extensions page
-  let legacyLink = mgrWin.document.getElementById("legacy-extensions-learnmore-link");
-  is_element_visible(legacyLink, "Link to legacy extensions is visible");
-
-  let loadPromise = new Promise(resolve => wait_for_view_load(mgrWin, resolve, true));
-  legacyLink.click();
-  await loadPromise;
-
-  is(mgrWin.gViewController.currentViewId, "addons://legacy/",
-     "Legacy extensions link leads to the correct view");
-
-  list = mgrWin.document.getElementById("legacy-list");
-  is(list.children.length, 1, "Should have 1 item in the legacy list");
-  item = list.children[0];
-  is(item.mAddon.id, ID, "Complete theme should be in the list");
-
-  // Click the find a replacement button
-  let button = document.getAnonymousElementByAttribute(item, "anonid", "replacement-btn");
-  is_element_visible(button, "Find a replacement butotn is visible");
-
-  // In automation, app.support.baseURL points to a page on localhost.
-  // The actual page is 404 in the test but that doesn't matter here,
-  // just the fact that we load the right URL.
-  let url = Services.prefs.getStringPref("app.support.baseURL") + "complete-themes";
-  let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url);
-  button.click();
-  let tab = await tabPromise;
-  ok(true, "Find a replacement button opened SUMO page");
-  BrowserTestUtils.removeTab(tab);
-
-  await close_manager(mgrWin);
-});