Bug 1421806 - Create a mixin to subscribe to payment store changes. r=jaws
MozReview-Commit-ID: IGvvx7JDRtP
--- 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>