Bug 1428414 - Use the autofill credit card form in the Payment dialog. r=jaws draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 22 Mar 2018 20:57:13 -0700
changeset 776695 9a1133aef407576ee1427b45e40161c9bfba897f
parent 776192 a46a935e7886e81d7e39fe2d606305bba8bdddac
child 776696 e7eecf00f49800e841535f17143acb2e9ae248fe
child 776772 029f58048cd68664d766b28ffb3445eaaccb5c93
child 776795 58d4cad0fb2d2670a90ba0935056d3ffbd4cf96f
push id104957
push usermozilla@noorenberghe.ca
push dateTue, 03 Apr 2018 15:51:18 +0000
reviewersjaws
bugs1428414
milestone61.0a1
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/icon64.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;