Bug 1428414 - Use the autofill credit card form in the Payment dialog. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 22 Mar 2018 20:57:13 -0700
changeset 411560 b045a7779f4ddc2a2dcd93cbfe187d4b880a1f57
parent 411559 4fa0e33621e398a08243851c72c6b5415decc722
child 411561 6045035ad2edad1ed5a8c336883db01aa73bd463
push id101686
push useraciure@mozilla.com
push dateTue, 03 Apr 2018 21:59:31 +0000
treeherdermozilla-inbound@8d846598d35d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1428414
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
Bug 1428414 - Use the autofill credit card form in the Payment dialog. r=jaws MozReview-Commit-ID: 4BjUfETLv0X
browser/installer/allowed-dupes.mn
toolkit/components/payments/content/paymentDialogFrameScript.js
toolkit/components/payments/jar.mn
toolkit/components/payments/moz.build
toolkit/components/payments/res/containers/basic-card-form.js
toolkit/components/payments/res/containers/payment-dialog.js
toolkit/components/payments/res/containers/payment-method-picker.js
toolkit/components/payments/res/log.js
toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
toolkit/components/payments/res/paymentRequest.css
toolkit/components/payments/res/paymentRequest.js
toolkit/components/payments/res/paymentRequest.xhtml
toolkit/components/payments/res/unprivileged-fallbacks.js
toolkit/components/payments/test/mochitest/formautofill/mochitest.ini
toolkit/components/payments/test/mochitest/formautofill/test_editCreditCard.html
toolkit/components/payments/test/mochitest/mochitest.ini
toolkit/components/payments/test/mochitest/payments_common.js
toolkit/components/payments/test/mochitest/test_basic_card_form.html
toolkit/components/payments/test/mochitest/test_payment_dialog.html
--- a/browser/installer/allowed-dupes.mn
+++ b/browser/installer/allowed-dupes.mn
@@ -142,8 +142,13 @@ res/table-remove-column.gif
 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/editCreditCard.xhtml
+chrome/toolkit/res/payments/formautofill/editCreditCard.xhtml
+browser/features/formautofill@mozilla.org/chrome/content/autofillEditForms.js
+chrome/toolkit/res/payments/formautofill/autofillEditForms.js
--- a/toolkit/components/payments/content/paymentDialogFrameScript.js
+++ b/toolkit/components/payments/content/paymentDialogFrameScript.js
@@ -16,16 +16,19 @@
  */
 
 "use strict";
 
 /* eslint-env mozilla/frame-script */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
+ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
+                               "resource://formautofill/FormAutofillUtils.jsm");
+
 let PaymentFrameScript = {
   init() {
     XPCOMUtils.defineLazyGetter(this, "log", () => {
       let {ConsoleAPI} = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
       return new ConsoleAPI({
         maxLogLevelPref: "dom.payments.loglevel",
         prefix: "paymentDialogFrameScript",
       });
@@ -53,20 +56,36 @@ let PaymentFrameScript = {
     let contentLogObject = Cu.waiveXrays(content).log;
     for (let name of ["error", "warn", "info", "debug"]) {
       Cu.exportFunction(privilegedLogger[name].bind(privilegedLogger), contentLogObject, {
         defineAs: name,
       });
     }
   },
 
+  /**
+   * Expose privileged utility functions to the unprivileged page.
+   */
+  exposeUtilityFunctions() {
+    let PaymentDialogUtils = {
+      isCCNumber(value) {
+        return FormAutofillUtils.isCCNumber(value);
+      },
+    };
+    let waivedContent = Cu.waiveXrays(content);
+    waivedContent.PaymentDialogUtils = Cu.cloneInto(PaymentDialogUtils, waivedContent, {
+      cloneFunctions: true,
+    });
+  },
+
   sendToChrome({detail}) {
     let {messageType} = detail;
     if (messageType == "initializeRequest") {
       this.setupContentConsole();
+      this.exposeUtilityFunctions();
     }
     this.log.debug("sendToChrome:", messageType, detail);
     this.sendMessageToChrome(messageType, detail);
   },
 
   sendToContent(messageType, detail = {}) {
     this.log.debug("sendToContent", messageType, detail);
     let response = Object.assign({messageType}, detail);
--- a/toolkit/components/payments/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -13,12 +13,14 @@ toolkit.jar:
     res/payments                                      (res/paymentRequest.*)
     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/log.js                               (res/log.js)
+    res/payments/formautofill/autofillEditForms.js    (../../../../browser/extensions/formautofill/content/autofillEditForms.js)
+    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/*)
--- a/toolkit/components/payments/moz.build
+++ b/toolkit/components/payments/moz.build
@@ -11,17 +11,20 @@ with Files('**'):
 
 EXTRA_COMPONENTS += [
     'payments.manifest',
     'paymentUIService.js',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
-MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
+MOCHITEST_MANIFESTS += [
+    'test/mochitest/formautofill/mochitest.ini',
+    'test/mochitest/mochitest.ini',
+]
 
 SPHINX_TREES['docs'] = 'docs'
 
 with Files('docs/**'):
     SCHEDULES.exclusive = ['docs']
 
 TESTING_JS_MODULES += [
     'test/PaymentTestUtils.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/containers/basic-card-form.js
@@ -0,0 +1,105 @@
+/* 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-globals-from ../mixins/PaymentStateSubscriberMixin.js */
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+"use strict";
+
+/**
+ * <basic-card-form></basic-card-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.
+ */
+
+class BasicCardForm extends PaymentStateSubscriberMixin(HTMLElement) {
+  constructor() {
+    super();
+
+    this.backButton = document.createElement("button");
+    this.backButton.addEventListener("click", this);
+
+    // The markup is shared with form autofill preferences.
+    let url = "formautofill/editCreditCard.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(form);
+
+      let record = {};
+      this.formHandler = new EditCreditCard({
+        form,
+      }, record, {
+        isCCNumber: PaymentDialogUtils.isCCNumber,
+      });
+
+      this.appendChild(this.backButton);
+      // 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;
+
+    let record = {};
+    let {
+      selectedPaymentCard,
+      savedBasicCards,
+    } = state;
+
+    let editing = !!state.selectedPaymentCard;
+    this.form.querySelector("#cc-number").disabled = editing;
+
+    // If a card is selected we want to edit it.
+    if (editing) {
+      record = savedBasicCards[selectedPaymentCard];
+      if (!record) {
+        throw new Error("Trying to edit a non-existing card: " + selectedPaymentCard);
+      }
+    }
+
+    this.formHandler.loadRecord(record);
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "click": {
+        this.onClick(event);
+        break;
+      }
+    }
+  }
+
+  onClick(evt) {
+    this.requestStore.setState({
+      page: {
+        id: "payment-summary",
+      },
+    });
+  }
+}
+
+customElements.define("basic-card-form", BasicCardForm);
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -25,17 +25,19 @@ class PaymentDialog extends PaymentState
     this._cancelButton.addEventListener("click", this.cancelRequest);
 
     this._payButton = contents.querySelector("#pay");
     this._payButton.addEventListener("click", this);
 
     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._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");
@@ -241,16 +243,20 @@ class PaymentDialog extends PaymentState
     }
 
     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;
+    }
+
     let {
       changesPrevented,
       completionState,
     } = state;
     if (changesPrevented) {
       this.setAttribute("changes-prevented", "");
     } else {
       this.removeAttribute("changes-prevented");
--- a/toolkit/components/payments/res/containers/payment-method-picker.js
+++ b/toolkit/components/payments/res/containers/payment-method-picker.js
@@ -3,36 +3,47 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* global PaymentStateSubscriberMixin */
 
 "use strict";
 
 /**
  * <payment-method-picker></payment-method-picker>
- * Container around <rich-select> (eventually providing add/edit links) with
+ * Container around add/edit links and <rich-select> with
  * <basic-card-option> listening to savedBasicCards.
  */
 
 class PaymentMethodPicker extends PaymentStateSubscriberMixin(HTMLElement) {
   constructor() {
     super();
     this.dropdown = document.createElement("rich-select");
     this.dropdown.addEventListener("change", this);
     this.spacerText = document.createTextNode(" ");
     this.securityCodeInput = document.createElement("input");
     this.securityCodeInput.autocomplete = "off";
     this.securityCodeInput.size = 3;
     this.securityCodeInput.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.spacerText);
     this.appendChild(this.securityCodeInput);
+    this.appendChild(this.addLink);
+    this.append(" ");
+    this.appendChild(this.editLink);
     super.connectedCallback();
   }
 
   render(state) {
     let {savedBasicCards} = state;
     let desiredOptions = [];
     for (let [guid, basicCard] of Object.entries(savedBasicCards)) {
       let optionEl = this.dropdown.getOptionByValue(guid);
@@ -69,16 +80,20 @@ class PaymentMethodPicker extends Paymen
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "change": {
         this.onChange(event);
         break;
       }
+      case "click": {
+        this.onClick(event);
+        break;
+      }
     }
   }
 
   onChange({target}) {
     let selectedKey = this.selectedStateKey;
     let stateChange = {};
 
     if (!selectedKey) {
@@ -99,11 +114,34 @@ class PaymentMethodPicker extends Paymen
       }
       default: {
         return;
       }
     }
 
     this.requestStore.setState(stateChange);
   }
+
+  onClick({target}) {
+    let nextState = {
+      page: {
+        id: "basic-card-page",
+      },
+    };
+
+    switch (target) {
+      case this.addLink: {
+        nextState.selectedPaymentCard = null;
+        break;
+      }
+      case this.editLink: {
+        break;
+      }
+      default: {
+        throw new Error("Unexpected onClick");
+      }
+    }
+
+    this.requestStore.setState(nextState);
+  }
 }
 
 customElements.define("payment-method-picker", PaymentMethodPicker);
--- a/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -12,16 +12,19 @@
 
 /**
  * State of the payment request dialog.
  */
 let requestStore = new PaymentsStore({
   changesPrevented: false,
   completionState: "initial",
   orderDetailsShowing: false,
+  page: {
+    id: "payment-summary",
+  },
   request: {
     tabId: null,
     topLevelPrincipal: {URI: {displayHost: null}},
     requestId: null,
     paymentMethods: [],
     paymentDetails: {
       id: null,
       totalItem: {label: null, amount: {currency: null, value: 0}},
--- a/toolkit/components/payments/res/paymentRequest.css
+++ b/toolkit/components/payments/res/paymentRequest.css
@@ -8,16 +8,20 @@ html {
 }
 
 body {
   height: 100%;
   margin: 0;
   overflow: hidden;
 }
 
+[hidden] {
+  display: none !important;
+}
+
 #debugging-console {
   float: right;
   /* Float above the other overlays */
   position: relative;
   z-index: 99;
 }
 
 payment-dialog {
--- a/toolkit/components/payments/res/paymentRequest.js
+++ b/toolkit/components/payments/res/paymentRequest.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /**
  * Loaded in the unprivileged frame of each payment dialog.
  *
  * Communicates with privileged code via DOM Events.
  */
 
-/* import-globals-from log.js */
+/* import-globals-from unprivileged-fallbacks.js */
 
 "use strict";
 
 var paymentRequest = {
   domReadyPromise: null,
 
   init() {
     // listen to content
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -1,62 +1,74 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 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/. -->
 <!DOCTYPE html [
+  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+  %globalDTD;
+
   <!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 basicCard.addLink.label    "Add">
+  <!ENTITY basicCard.editLink.label   "Edit">
   <!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.backButton.label     "Back">
 ]>
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
-  <meta http-equiv="Content-Security-Policy" content="default-src 'self'"/>
-  <title></title>
+  <title>&paymentSummaryTitle;</title>
+
+  <!-- chrome: is needed for global.dtd -->
+  <meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
+
   <link rel="stylesheet" href="paymentRequest.css"/>
   <link rel="stylesheet" href="components/rich-select.css"/>
   <link rel="stylesheet" href="components/address-option.css"/>
   <link rel="stylesheet" href="components/basic-card-option.css"/>
   <link rel="stylesheet" href="components/shipping-option.css"/>
   <link rel="stylesheet" href="components/payment-details-item.css"/>
   <link rel="stylesheet" href="containers/order-details.css"/>
 
   <script src="vendor/custom-elements.min.js"></script>
 
-  <script src="log.js"></script>
+  <script src="unprivileged-fallbacks.js"></script>
 
   <script src="PaymentsStore.js"></script>
 
   <script src="mixins/ObservedPropertiesMixin.js"></script>
   <script src="mixins/PaymentStateSubscriberMixin.js"></script>
 
+  <script src="formautofill/autofillEditForms.js"></script>
+
   <script src="components/currency-amount.js"></script>
   <script src="containers/order-details.js"></script>
   <script src="components/payment-details-item.js"></script>
   <script src="components/rich-select.js"></script>
   <script src="components/rich-option.js"></script>
   <script src="components/address-option.js"></script>
   <script src="components/shipping-option.js"></script>
   <script src="containers/address-picker.js"></script>
   <script src="components/basic-card-option.js"></script>
   <script src="containers/shipping-option-picker.js"></script>
   <script src="containers/payment-method-picker.js"></script>
+  <script src="containers/basic-card-form.js"></script>
   <script src="containers/payment-dialog.js"></script>
 
   <script src="paymentRequest.js"></script>
 
   <template id="payment-dialog-template">
     <header>
       <div id="total">
         <h2 class="label"></h2>
@@ -64,34 +76,37 @@
         <div id="host-name"></div>
       </div>
       <div id="top-buttons" >
         <button id="view-all" class="closed">&viewAllItems;</button>
       </div>
     </header>
 
     <div id="main-container">
-      <section id="payment-summary">
+      <section id="payment-summary" class="page">
         <h1>&paymentSummaryTitle;</h1>
 
         <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>
 
           <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"></payment-method-picker>
+          <payment-method-picker selected-state-key="selectedPaymentCard"
+                                 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"
                           selected-state-key="selectedPayerAddress"></address-picker>
           <div id="error-text"></div>
         </section>
 
         <footer id="controls-container">
@@ -103,16 +118,21 @@
                   data-unknown-label="&unknownPaymentButton.label;"
                   data-success-label="&successPaymentButton.label;"></button>
         </footer>
       </section>
       <section id="order-details-overlay" hidden="hidden">
         <h1>&orderDetailsLabel;</h1>
         <order-details></order-details>
       </section>
+
+      <basic-card-form id="basic-card-page"
+                       class="page"
+                       data-back-button-label="&basicCardPage.backButton.label;"
+                       hidden="hidden"></basic-card-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">
@@ -120,16 +140,16 @@
     <ul class="footer-items-list"></ul>
 
     <div class="details-total">
       <h2 class="label">&orderTotalLabel;</h2>
       <currency-amount></currency-amount>
     </div>
   </template>
 </head>
-<body>
+<body dir="&locale.dir;">
   <iframe id="debugging-console"
           hidden="hidden"
           height="400"
           src="debugging.html"></iframe>
   <payment-dialog></payment-dialog>
 </body>
 </html>
rename from toolkit/components/payments/res/log.js
rename to toolkit/components/payments/res/unprivileged-fallbacks.js
--- a/toolkit/components/payments/res/log.js
+++ b/toolkit/components/payments/res/unprivileged-fallbacks.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/. */
 
 /**
- * This file defines a fallback log object to be used during development outside
+ * This file defines fallback objects to be used during development outside
  * of the paymentDialogWrapper. When loaded in the wrapper, a frame script
- * providing pref-controlled logging overwrites these methods.
+ * overwrites these methods.
  */
 
 /* eslint-disable no-console */
-/* exported log */
+/* exported log, PaymentDialogUtils */
 
 "use strict";
 
 var log = {
   error(...args) {
     console.error("log.js", ...args);
   },
   warn(...args) {
@@ -22,8 +22,14 @@ var log = {
   },
   info(...args) {
     console.info("log.js", ...args);
   },
   debug(...args) {
     console.debug("log.js", ...args);
   },
 };
+
+var PaymentDialogUtils = {
+  isCCNumber(str) {
+    return str.length > 0;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/formautofill/mochitest.ini
@@ -0,0 +1,8 @@
+[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
+
+[test_editCreditCard.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/formautofill/test_editCreditCard.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test that editCreditCard.xhtml is accessible for tests in the parent directory.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test that editCreditCard.xhtml is accessible</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/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+    <iframe id="editCreditCard" src="editCreditCard.xhtml"></iframe>
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+
+add_task(async function test_editCreditCard() {
+  let editCreditCard = document.getElementById("editCreditCard").contentWindow;
+  await SimpleTest.promiseFocus(editCreditCard);
+  ok(editCreditCard.document.getElementById("form"), "Check form is present");
+  ok(editCreditCard.document.getElementById("cc-number"), "Check cc-number is present");
+});
+
+</script>
+
+</body>
+</html>
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -1,40 +1,44 @@
 [DEFAULT]
 prefs =
    dom.webcomponents.customelements.enabled=false
 support-files =
+   ../../../../../browser/extensions/formautofill/content/autofillEditForms.js
    ../../../../../testing/modules/sinon-2.3.2.js
    ../../res/paymentRequest.css
    ../../res/paymentRequest.xhtml
    ../../res/PaymentsStore.js
+   ../../res/unprivileged-fallbacks.js
    ../../res/components/currency-amount.js
    ../../res/components/address-option.js
    ../../res/components/address-option.css
    ../../res/components/basic-card-option.js
    ../../res/components/basic-card-option.css
    ../../res/components/payment-details-item.js
    ../../res/components/rich-option.js
    ../../res/components/rich-select.css
    ../../res/components/rich-select.js
    ../../res/components/shipping-option.js
    ../../res/components/shipping-option.css
    ../../res/containers/address-picker.js
+   ../../res/containers/basic-card-form.js
    ../../res/containers/shipping-option-picker.js
    ../../res/containers/order-details.js
    ../../res/containers/payment-dialog.js
    ../../res/containers/payment-method-picker.js
    ../../res/mixins/ObservedPropertiesMixin.js
    ../../res/mixins/PaymentStateSubscriberMixin.js
    ../../res/vendor/custom-elements.min.js
    ../../res/vendor/custom-elements.min.js.map
    payments_common.js
 skip-if = !e10s
 
 [test_address_picker.html]
+[test_basic_card_form.html]
 [test_currency_amount.html]
 [test_order_details.html]
 [test_payer_address_picker.html]
 [test_payment_dialog.html]
 [test_payment_details_item.html]
 [test_payment_method_picker.html]
 [test_rich_select.html]
 [test_shipping_option_picker.html]
--- a/toolkit/components/payments/test/mochitest/payments_common.js
+++ b/toolkit/components/payments/test/mochitest/payments_common.js
@@ -1,11 +1,14 @@
 "use strict";
 
-/* exported asyncElementRendered, promiseStateChange, deepClone */
+/* exported asyncElementRendered, promiseStateChange, deepClone, PTU */
+
+const PTU = SpecialPowers.Cu.import("resource://testing-common/PaymentTestUtils.jsm", {})
+                            .PaymentTestUtils;
 
 /**
  * A helper to await on while waiting for an asynchronous rendering of a Custom
  * Element.
  * @returns {Promise}
  */
 function asyncElementRendered() {
   return Promise.resolve();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/test_basic_card_form.html
@@ -0,0 +1,146 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the basic-card-form element
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the basic-card-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/SpawnTask.js"></script>
+  <script src="sinon-2.3.2.js"></script>
+  <script src="payments_common.js"></script>
+  <script src="custom-elements.min.js"></script>
+  <script src="unprivileged-fallbacks.js"></script>
+  <script src="PaymentsStore.js"></script>
+  <script src="PaymentStateSubscriberMixin.js"></script>
+  <script src="autofillEditForms.js"></script>
+  <script src="basic-card-form.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <link rel="stylesheet" type="text/css" href="paymentRequest.css"/>
+</head>
+<body>
+  <p id="display">
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test the basic-card-form element **/
+
+/* global sinon */
+/* import-globals-from payments_common.js */
+/* import-globals-from ../../res/mixins/PaymentStateSubscriberMixin.js */
+
+let display = document.getElementById("display");
+
+function checkCCForm(customEl, expectedCard) {
+  const CC_PROPERTY_NAMES = [
+    "cc-number",
+    "cc-name",
+    "cc-exp-month",
+    "cc-exp-year",
+  ];
+  for (let propName of CC_PROPERTY_NAMES) {
+    let expectedVal = expectedCard[propName] || "";
+    is(document.getElementById(propName).value,
+       expectedVal.toString(),
+       `Check ${propName}`);
+  }
+}
+
+add_task(async function test_initialState() {
+  let form = document.createElement("basic-card-form");
+  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 = document.createElement("basic-card-form");
+  form.dataset.backButtonLabel = "Back";
+  await form.requestStore.setState({
+    page: {
+      id: "test-page",
+    },
+  });
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let stateChangePromise = promiseStateChange(form.requestStore);
+  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();
+});
+
+add_task(async function test_record() {
+  let form = document.createElement("basic-card-form");
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  info("test year before current");
+  let card1 = deepClone(PTU.BasicCards.JohnDoe);
+  card1.guid = "9864798564";
+  card1["cc-exp-year"] = 2011;
+
+  await form.requestStore.setState({
+    selectedPaymentCard: card1.guid,
+    savedBasicCards: {
+      [card1.guid]: deepClone(card1),
+    },
+  });
+  await asyncElementRendered();
+  checkCCForm(form, card1);
+
+  info("test future year");
+  card1["cc-exp-year"] = 2100;
+
+  await form.requestStore.setState({
+    savedBasicCards: {
+      [card1.guid]: deepClone(card1),
+    },
+  });
+  await asyncElementRendered();
+  checkCCForm(form, card1);
+
+  info("test change to minimal record");
+  let minimalCard = {
+    // no expiration date or name
+    "cc-number": "1234567690123",
+    guid: "9gnjdhen46",
+  };
+  await form.requestStore.setState({
+    selectedPaymentCard: minimalCard.guid,
+    savedBasicCards: {
+      [minimalCard.guid]: deepClone(minimalCard),
+    },
+  });
+  await asyncElementRendered();
+  checkCCForm(form, minimalCard);
+
+  info("change to no selected card");
+  await form.requestStore.setState({
+    selectedPaymentCard: null,
+  });
+  await asyncElementRendered();
+  checkCCForm(form, {});
+
+  form.remove();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/payments/test/mochitest/test_payment_dialog.html
+++ b/toolkit/components/payments/test/mochitest/test_payment_dialog.html
@@ -82,16 +82,17 @@ async function setup() {
 
 add_task(async function test_initialState() {
   await setup();
   let initialState = el1.requestStore.getState();
   let elDetails = el1._orderDetailsOverlay;
 
   is(initialState.orderDetailsShowing, false, "orderDetailsShowing is initially false");
   ok(elDetails.hasAttribute("hidden"), "Check details are hidden");
+  is(initialState.page.id, "payment-summary", "Check initial page");
 });
 
 add_task(async function test_viewAllButton() {
   await setup();
 
   let elDetails = el1._orderDetailsOverlay;
   let button = el1._viewAllButton;