Bug 1421806 - Create a mixin to subscribe to payment store changes. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Thu, 30 Nov 2017 14:37:09 -0800
changeset 449205 562dbac7f65ce1a567c715981541fab3e56c9d52
parent 449204 a68d5f4b11c097514273f273dfdfcc2b074c2fa1
child 449206 bd96153b3e7c4503b60cd9eeda847a12492d0b87
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1421806
milestone59.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 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>