Bug 1421806 - Create a mixin to subscribe to payment store changes. r=jaws draft
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 30 Nov 2017 14:37:09 -0800
changeset 706429 33452110c0810194c825af76efa4201c047bfea5
parent 706036 09dbe6851331dad7b7e81b58c65ac6f6c2eb28f6
child 706430 e26dbd565f22002fb684777e3c95395666eb6359
push id91798
push usermozilla@noorenberghe.ca
push dateFri, 01 Dec 2017 22:15:35 +0000
reviewersjaws
bugs1421806
milestone59.0a1
Bug 1421806 - Create a mixin to subscribe to payment store changes. r=jaws MozReview-Commit-ID: IGvvx7JDRtP
toolkit/components/payments/jar.mn
toolkit/components/payments/res/mixins/ObservedPropertiesMixin.js
toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
toolkit/components/payments/test/mochitest/mochitest.ini
toolkit/components/payments/test/mochitest/test_PaymentStateSubscriberMixin.html
--- a/toolkit/components/payments/jar.mn
+++ b/toolkit/components/payments/jar.mn
@@ -6,13 +6,14 @@ toolkit.jar:
 %   content payments %content/payments/
     content/payments/paymentDialog.css                (content/paymentDialog.css)
     content/payments/paymentDialog.js                 (content/paymentDialog.js)
     content/payments/paymentDialogFrameScript.js      (content/paymentDialogFrameScript.js)
     content/payments/paymentDialog.xhtml              (content/paymentDialog.xhtml)
 %   resource payments %res/payments/
     res/payments                                      (res/paymentRequest.*)
     res/payments/components/                          (res/components/*.js)
+    res/payments/containers/                          (res/containers/*.js)
     res/payments/debugging.html                       (res/debugging.html)
     res/payments/debugging.js                         (res/debugging.js)
     res/payments/mixins/                              (res/mixins/*.js)
     res/payments/PaymentsStore.js                     (res/PaymentsStore.js)
     res/payments/vendor/                              (res/vendor/*)
--- a/toolkit/components/payments/res/mixins/ObservedPropertiesMixin.js
+++ b/toolkit/components/payments/res/mixins/ObservedPropertiesMixin.js
@@ -52,15 +52,18 @@ function ObservedPropertiesMixin(superCl
       try {
         this.render();
       } finally {
         this._observedPropertiesMixin.pendingRender = false;
       }
     }
 
     attributeChangedCallback(attr, oldValue, newValue) {
+      if (super.attributeChangedCallback) {
+        super.attributeChangedCallback(attr, oldValue, newValue);
+      }
       if (oldValue === newValue) {
         return;
       }
       this._invalidateFromObservedPropertiesMixin();
     }
   };
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global PaymentsStore */
+
+/**
+ * A mixin for a custom element to observe store changes to information about a payment request.
+ */
+
+/**
+ * State of the payment request dialog.
+ */
+let requestStore = new PaymentsStore({
+  request: {
+    tabId: null,
+    topLevelPrincipal: {URI: {displayHost: null}},
+    requestId: null,
+    paymentMethods: [],
+    paymentDetails: {
+      id: null,
+      totalItem: {label: null, amount: {currency: null, value: null}},
+      displayItems: [],
+      shippingOptions: [],
+      modifiers: null,
+      error: "",
+    },
+    paymentOptions: {
+      requestPayerName: false,
+      requestPayerEmail: false,
+      requestPayerPhone: false,
+      requestShipping: false,
+      shippingType: "shipping",
+    },
+  },
+  savedAddresses: [],
+  savedBasicCards: [],
+});
+
+
+/* exported PaymentStateSubscriberMixin */
+
+/**
+ * A mixin to render UI based upon the requestStore and get updated when that store changes.
+ *
+ * Attaches `requestStore` to the element to give access to the store.
+ * @param {class} superClass The class to extend
+ * @returns {class}
+ */
+function PaymentStateSubscriberMixin(superClass) {
+  return class PaymentStateSubscriber extends superClass {
+    constructor() {
+      super();
+      this.requestStore = requestStore;
+    }
+
+    connectedCallback() {
+      this.requestStore.subscribe(this);
+      this.render(this.requestStore.getState());
+      if (super.connectedCallback) {
+        super.connectedCallback();
+      }
+    }
+
+    disconnectedCallback() {
+      this.requestStore.unsubscribe(this);
+      if (super.disconnectedCallback) {
+        super.disconnectedCallback();
+      }
+    }
+
+    /**
+     * Called by the store upon state changes.
+     * @param {object} state The current state
+     */
+    stateChangeCallback(state) {
+      this.render(state);
+    }
+  };
+}
--- a/toolkit/components/payments/test/mochitest/mochitest.ini
+++ b/toolkit/components/payments/test/mochitest/mochitest.ini
@@ -1,10 +1,14 @@
 [DEFAULT]
 support-files =
+   ../../../../../testing/modules/sinon-2.3.2.js
+   ../../res/PaymentsStore.js
    ../../res/components/currency-amount.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
 
 [test_currency_amount.html]
 [test_ObservedPropertiesMixin.html]
+[test_PaymentStateSubscriberMixin.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/mochitest/test_PaymentStateSubscriberMixin.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the PaymentStateSubscriberMixin
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test the PaymentStateSubscriberMixin</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.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="PaymentsStore.js"></script>
+  <script src="PaymentStateSubscriberMixin.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p id="display">
+    <test-element id="el1"></test-element>
+  </p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+<script type="application/javascript">
+/** Test the PaymentStateSubscriberMixin **/
+
+/* global sinon */
+/* import-globals-from payments_common.js */
+/* import-globals-from ../../res/mixins/PaymentStateSubscriberMixin.js */
+
+class TestElement extends PaymentStateSubscriberMixin(HTMLElement) {
+  render(state) {
+    this.textContent = JSON.stringify(state);
+  }
+}
+
+// We must spy on the prototype by creating the instance in order to test Custom Element reactions.
+sinon.spy(TestElement.prototype, "disconnectedCallback");
+
+customElements.define("test-element", TestElement);
+let el1 = document.getElementById("el1");
+
+sinon.spy(el1, "render");
+sinon.spy(el1, "stateChangeCallback");
+
+add_task(async function test_initialState() {
+  let parsedState = JSON.parse(el1.textContent);
+  ok(!!parsedState.request, "Check initial state contains `request`");
+  ok(!!parsedState.savedAddresses, "Check initial state contains `savedAddresses`");
+  ok(!!parsedState.savedBasicCards, "Check initial state contains `savedBasicCards`");
+});
+
+add_task(async function test_async_batched_render() {
+  el1.requestStore.setState({a: 1});
+  el1.requestStore.setState({b: 2});
+  await asyncElementRendered();
+  ok(el1.stateChangeCallback.calledOnce, "stateChangeCallback called once");
+  ok(el1.render.calledOnce, "render called once");
+
+  let parsedState = JSON.parse(el1.textContent);
+  is(parsedState.a, 1, "Check a");
+  is(parsedState.b, 2, "Check b");
+});
+
+add_task(async function test_disconnect() {
+  el1.disconnectedCallback.reset();
+  el1.render.reset();
+  el1.stateChangeCallback.reset();
+  el1.remove();
+  ok(el1.disconnectedCallback.calledOnce, "disconnectedCallback called once");
+  await el1.requestStore.setState({a: 3});
+  await asyncElementRendered();
+  ok(el1.stateChangeCallback.notCalled, "stateChangeCallback not called");
+  ok(el1.render.notCalled, "render not called");
+});
+</script>
+
+</body>
+</html>