Bug 1418226 - Create a store for payment dialog unprivileged UI state. r=jaws

MozReview-Commit-ID: HHEYdXahhcI

--HG--
extra : rebase_source : 09dbe6851331dad7b7e81b58c65ac6f6c2eb28f6
This commit is contained in:
Matthew Noorenberghe 2017-11-30 18:13:21 -08:00
Родитель f208c7a958
Коммит 25a9e2348c
9 изменённых файлов: 248 добавлений и 2 удалений

Просмотреть файл

@ -20,6 +20,9 @@ var gExceptionPaths = [
"resource://gre/defaults/pref/",
"resource://shield-recipe-client/node_modules/jexl/lib/",
// These resources are referenced using relative paths from html files.
"resource://payments/",
// https://github.com/mozilla/normandy/issues/577
"resource://shield-recipe-client/test/",

Просмотреть файл

@ -30,7 +30,7 @@ module.exports = {
"max-len": ["error", 100],
"max-nested-callbacks": ["error", 4],
"new-parens": "error",
"no-console": "error",
"no-console": ["error", { allow: ["error"] }],
"no-fallthrough": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": ["error", {

Просмотреть файл

@ -10,8 +10,9 @@ toolkit.jar:
content/payments/paymentDialog.xhtml (content/paymentDialog.xhtml)
% resource payments %res/payments/
res/payments (res/paymentRequest.*)
res/payments/components/ (res/components/*.js)
res/payments/debugging.html (res/debugging.html)
res/payments/debugging.js (res/debugging.js)
res/payments/components/ (res/components/*.js)
res/payments/mixins/ (res/mixins/*.js)
res/payments/PaymentsStore.js (res/PaymentsStore.js)
res/payments/vendor/ (res/vendor/*)

Просмотреть файл

@ -23,3 +23,5 @@ SPHINX_TREES['docs'] = 'docs'
TESTING_JS_MODULES += [
'test/PaymentTestUtils.jsm',
]
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']

Просмотреть файл

@ -0,0 +1,92 @@
/* 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";
/**
* The PaymentsStore class provides lightweight storage with an async publish/subscribe mechanism.
* Synchronous state changes are batched to improve application performance and to reduce partial
* state propagation.
*/
/* exported PaymentsStore */
class PaymentsStore {
/**
* @param {object} [defaultState = {}] The initial state of the store.
*/
constructor(defaultState = {}) {
this._state = defaultState;
this._nextNotifification = 0;
this._subscribers = new Set();
}
/**
* Get the current state as a shallow clone with a shallow freeze.
* You shouldn't modify any part of the returned state object as that would bypass notifying
* subscribers and could lead to subscribers assuming old state.
*
* @returns {Object} containing the current state
*/
getState() {
return Object.freeze(Object.assign({}, this._state));
}
/**
* Augment the current state with the keys of `obj` and asynchronously notify
* state subscribers. As a result, multiple synchronous state changes will lead
* to a single subscriber notification which leads to better performance and
* reduces partial state changes.
*
* @param {Object} obj The object to augment the state with. Keys in the object
* will be shallow copied with Object.assign.
*
* @example If the state is currently {a:3} then setState({b:"abc"}) will result in a state of
* {a:3, b:"abc"}.
*/
async setState(obj) {
Object.assign(this._state, obj);
let thisChangeNum = ++this._nextNotifification;
// Let any synchronous setState calls that happen after the current setState call
// complete first.
// Their effects on the state will be batched up before the callback is actually called below.
await Promise.resolve();
// Don't notify for state changes that are no longer the most recent. We only want to call the
// callback once with the latest state.
if (thisChangeNum !== this._nextNotifification) {
return;
}
for (let subscriber of this._subscribers) {
try {
subscriber.stateChangeCallback(this.getState());
} catch (ex) {
console.error(ex);
}
}
}
/**
* Subscribe the object to state changes notifications via a `stateChangeCallback` method.
*
* @param {Object} component to receive state change callbacks via a `stateChangeCallback` method.
* If the component is already subscribed, do nothing.
*/
subscribe(component) {
if (this._subscribers.has(component)) {
return;
}
this._subscribers.add(component);
}
/**
* @param {Object} component to stop receiving state change callbacks.
*/
unsubscribe(component) {
this._subscribers.delete(component);
}
}

Просмотреть файл

@ -0,0 +1,7 @@
"use strict";
module.exports = {
"extends": [
"plugin:mozilla/xpcshell-test"
]
};

Просмотреть файл

@ -0,0 +1,11 @@
const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
// ================================================
// Load mocking/stubbing library, sinon
// docs: http://sinonjs.org/releases/v2.3.2/
Cu.import("resource://gre/modules/Timer.jsm");
Services.scriptloader.loadSubScript("resource://testing-common/sinon-2.3.2.js", this);
/* globals sinon */
// ================================================

Просмотреть файл

@ -0,0 +1,126 @@
"use strict";
/* import-globals-from ../../res/PaymentsStore.js */
Services.scriptloader.loadSubScript("resource://payments/PaymentsStore.js", this);
add_task(async function test_defaultState() {
do_check_true(!!PaymentsStore, "Check PaymentsStore import");
let ps = new PaymentsStore({
foo: "bar",
});
let state = ps.getState();
do_check_true(!!state, "Check state is truthy");
do_check_eq(state.foo, "bar", "Check .foo");
Assert.throws(() => state.foo = "new", TypeError, "Assigning to existing prop. should throw");
Assert.throws(() => state.other = "something", TypeError, "Adding a new prop. should throw");
Assert.throws(() => delete state.foo, TypeError, "Deleting a prop. should throw");
});
add_task(async function test_setState() {
let ps = new PaymentsStore({});
ps.setState({
one: "one",
});
let state = ps.getState();
do_check_eq(Object.keys(state).length, 1, "Should only have 1 prop. set");
do_check_eq(state.one, "one", "Check .one");
ps.setState({
two: 2,
});
state = ps.getState();
do_check_eq(Object.keys(state).length, 2, "Should have 2 props. set");
do_check_eq(state.one, "one", "Check .one");
do_check_eq(state.two, 2, "Check .two");
ps.setState({
one: "a",
two: "b",
});
state = ps.getState();
do_check_eq(state.one, "a", "Check .one");
do_check_eq(state.two, "b", "Check .two");
do_print("check consecutive setState for the same prop");
ps.setState({
one: "c",
});
ps.setState({
one: "d",
});
state = ps.getState();
do_check_eq(Object.keys(state).length, 2, "Should have 2 props. set");
do_check_eq(state.one, "d", "Check .one");
do_check_eq(state.two, "b", "Check .two");
});
add_task(async function test_subscribe_unsubscribe() {
let ps = new PaymentsStore({});
let subscriber = {
stateChangePromise: null,
_stateChangeResolver: null,
reset() {
this.stateChangePromise = new Promise(resolve => {
this._stateChangeResolver = resolve;
});
},
stateChangeCallback(state) {
this._stateChangeResolver(state);
this.stateChangePromise = new Promise(resolve => {
this._stateChangeResolver = resolve;
});
},
};
sinon.spy(subscriber, "stateChangeCallback");
subscriber.reset();
ps.subscribe(subscriber);
do_print("subscribe the same listener twice to ensure it still doesn't call the callback");
ps.subscribe(subscriber);
do_check_true(subscriber.stateChangeCallback.notCalled,
"Check not called synchronously when subscribing");
let changePromise = subscriber.stateChangePromise;
ps.setState({
a: 1,
});
do_check_true(subscriber.stateChangeCallback.notCalled,
"Check not called synchronously for changes");
let state = await changePromise;
do_check_eq(state, subscriber.stateChangeCallback.getCall(0).args[0],
"Check resolved state is last state");
do_check_eq(JSON.stringify(state), `{"a":1}`, "Check callback state");
do_print("Testing consecutive setState");
subscriber.reset();
subscriber.stateChangeCallback.reset();
changePromise = subscriber.stateChangePromise;
ps.setState({
a: 2,
});
ps.setState({
a: 3,
});
do_check_true(subscriber.stateChangeCallback.notCalled,
"Check not called synchronously for changes");
state = await changePromise;
do_check_eq(state, subscriber.stateChangeCallback.getCall(0).args[0],
"Check resolved state is last state");
do_check_eq(JSON.stringify(subscriber.stateChangeCallback.getCall(0).args[0]), `{"a":3}`,
"Check callback state matches second setState");
do_print("test unsubscribe");
subscriber.stateChangeCallback = function unexpectedChange() {
do_check_true(false, "stateChangeCallback shouldn't be called after unsubscribing");
};
ps.unsubscribe(subscriber);
ps.setState({
a: 4,
});
await Promise.resolve("giving a chance for the callback to be called");
});

Просмотреть файл

@ -0,0 +1,4 @@
[DEFAULT]
head = head.js
[test_PaymentsStore.js]