/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { Store } = ChromeUtils.importESModule(
"resource://activity-stream/lib/Store.sys.mjs"
);
const { ActivityStreamMessageChannel } = ChromeUtils.importESModule(
"resource://activity-stream/lib/ActivityStreamMessageChannel.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
// This creates the Redux top-level object.
/* globals Redux */
Services.scriptloader.loadSubScript(
"chrome://global/content/vendor/redux.js",
this
);
add_task(async
function test_expected_properties() {
let sandbox = sinon.createSandbox();
let store =
new Store();
Assert.equal(store.feeds.constructor.name,
"Map",
"Should create a Map");
Assert.equal(store.feeds.size, 0,
"Store should start without any feeds.");
Assert.ok(store._store,
"Has a ._store");
Assert.ok(store.dispatch,
"Has a .dispatch");
Assert.ok(store.getState,
"Has a .getState");
sandbox.restore();
});
add_task(async
function test_messagechannel() {
let sandbox = sinon.createSandbox();
sandbox
.stub(ActivityStreamMessageChannel.prototype,
"middleware")
.returns(() => next => action => next(action));
let store =
new Store();
info(
"Store should create a ActivityStreamMessageChannel with the right dispatcher"
);
Assert.ok(store.getMessageChannel(),
"Has a message channel");
Assert.equal(
store.getMessageChannel().dispatch,
store.dispatch,
"MessageChannel.dispatch forwards to store.dispatch"
);
Assert.equal(
store.getMessageChannel(),
store._messageChannel,
"_messageChannel is the member for getMessageChannel()"
);
store.dispatch({ type:
"FOO" });
Assert.ok(
ActivityStreamMessageChannel.prototype.middleware.calledOnce,
"Middleware called."
);
sandbox.restore();
});
add_task(async
function test_initFeed_add_feeds() {
info(
"Store.initFeed should add an instance of the feed to .feeds");
let sandbox = sinon.createSandbox();
let store =
new Store();
class Foo {}
store._prefs.set(
"foo",
true);
await store.init(
new Map([[
"foo", () =>
new Foo()]]));
store.initFeed(
"foo");
Assert.ok(store.feeds.has(
"foo"),
"foo is set");
Assert.ok(store.feeds.get(
"foo")
instanceof Foo,
"Got registered class");
sandbox.restore();
});
add_task(async
function test_initFeed_calls_onAction() {
info(
"Store should call the feed's onAction with uninit action if it exists");
let sandbox = sinon.createSandbox();
let store =
new Store();
let testFeed;
let createTestFeed = () => {
testFeed = { onAction: sandbox.spy() };
return testFeed;
};
const action = { type:
"FOO" };
store._feedFactories =
new Map([[
"test", createTestFeed]]);
store.initFeed(
"test", action);
Assert.ok(testFeed.onAction.calledOnce,
"onAction called");
Assert.ok(
testFeed.onAction.calledWith(action),
"onAction called with test action"
);
info(
"Store should add a .store property to the feed");
Assert.ok(testFeed.store,
"Store exists");
Assert.equal(testFeed.store, store,
"Feed store is the Store");
sandbox.restore();
});
add_task(async
function test_initFeed_on_init() {
info(
"Store should call .initFeed with each key");
let sandbox = sinon.createSandbox();
let store =
new Store();
sandbox.stub(store,
"initFeed");
store._prefs.set(
"foo",
true);
store._prefs.set(
"bar",
true);
await store.init(
new Map([
[
"foo", () => {}],
[
"bar", () => {}],
])
);
Assert.ok(store.initFeed.calledWith(
"foo"),
"First test feed initted");
Assert.ok(store.initFeed.calledWith(
"bar"),
"Second test feed initted");
sandbox.restore();
});
add_task(async
function test_disabled_feed() {
info(
"Store should not initialize the feed if the Pref is set to false");
let sandbox = sinon.createSandbox();
let store =
new Store();
sandbox.stub(store,
"initFeed");
store._prefs.set(
"foo",
false);
await store.init(
new Map([[
"foo", () => {}]]));
Assert.ok(store.initFeed.notCalled,
".initFeed not called");
store._prefs.set(
"foo",
true);
sandbox.restore();
});
add_task(async
function test_observe_pref_branch() {
info(
"Store should observe the pref branch");
let sandbox = sinon.createSandbox();
let store =
new Store();
sandbox.stub(store._prefs,
"observeBranch");
await store.init(
new Map());
Assert.ok(store._prefs.observeBranch.calledOnce,
"observeBranch called once");
Assert.ok(
store._prefs.observeBranch.calledWith(store),
"observeBranch passed the store"
);
sandbox.restore();
});
add_task(async
function test_emit_initial_event() {
info(
"Store should emit an initial event if provided");
let sandbox = sinon.createSandbox();
let store =
new Store();
const action = { type:
"FOO" };
sandbox.stub(store,
"dispatch");
await store.init(
new Map(), action);
Assert.ok(store.dispatch.calledOnce,
"Dispatch called once");
Assert.ok(store.dispatch.calledWith(action),
"Dispatch called with action");
sandbox.restore();
});
add_task(async
function test_initialize_telemetry_feed_first() {
info(
"Store should initialize the telemetry feed first");
let sandbox = sinon.createSandbox();
let store =
new Store();
store._prefs.set(
"feeds.foo",
true);
store._prefs.set(
"feeds.telemetry",
true);
const telemetrySpy = sandbox.stub().returns({});
const fooSpy = sandbox.stub().returns({});
// Intentionally put the telemetry feed as the second item.
const feedFactories =
new Map([
[
"feeds.foo", fooSpy],
[
"feeds.telemetry", telemetrySpy],
]);
await store.init(feedFactories);
Assert.ok(telemetrySpy.calledBefore(fooSpy),
"Telemetry feed initted first");
sandbox.restore();
});
add_task(async
function test_dispatch_init_load_events() {
info(
"Store should dispatch init/load events");
let sandbox = sinon.createSandbox();
let store =
new Store();
sandbox.stub(store.getMessageChannel(),
"simulateMessagesForExistingTabs");
await store.init(
new Map(), { type:
"FOO" });
Assert.ok(
store.getMessageChannel().simulateMessagesForExistingTabs.calledOnce,
"simulateMessagesForExistingTabs called once"
);
sandbox.restore();
});
add_task(async
function test_init_before_load() {
info(
"Store should dispatch INIT before LOAD");
let sandbox = sinon.createSandbox();
let store =
new Store();
sandbox.stub(store.getMessageChannel(),
"simulateMessagesForExistingTabs");
sandbox.stub(store,
"dispatch");
const init = { type:
"INIT" };
const load = { type:
"TAB_LOAD" };
store
.getMessageChannel()
.simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load));
await store.init(
new Map(), init);
Assert.ok(store.dispatch.calledTwice,
"Dispatch called twice");
Assert.equal(
store.dispatch.firstCall.args[0],
init,
"First dispatch was for init event"
);
Assert.equal(
store.dispatch.secondCall.args[0],
load,
"Second dispatch was for load event"
);
sandbox.restore();
});
add_task(async
function test_uninit_feeds() {
info(
"uninitFeed should not throw if no feed with that name exists");
let sandbox = sinon.createSandbox();
let store =
new Store();
try {
store.uninitFeed(
"does-not-exist");
Assert.ok(
true,
"Didn't throw");
}
catch (e) {
Assert.ok(
false,
"Should not have thrown");
}
info(
"uninitFeed should call the feed's onAction with uninit action if it exists"
);
let feed;
function createFeed() {
feed = { onAction: sandbox.spy() };
return feed;
}
const action = { type:
"BAR" };
store._feedFactories =
new Map([[
"foo", createFeed]]);
store.initFeed(
"foo");
store.uninitFeed(
"foo", action);
Assert.ok(feed.onAction.calledOnce);
Assert.ok(feed.onAction.calledWith(action));
info(
"uninitFeed should remove the feed from .feeds");
Assert.ok(!store.feeds.has(
"foo"),
"foo is not in .feeds");
sandbox.restore();
});
add_task(async
function test_onPrefChanged() {
let sandbox = sinon.createSandbox();
let store =
new Store();
let initFeedStub = sandbox.stub(store,
"initFeed");
let uninitFeedStub = sandbox.stub(store,
"uninitFeed");
store._prefs.set(
"foo",
false);
store.init(
new Map([[
"foo", () => ({})]]));
info(
"onPrefChanged should initialize the feed if called with true");
store.onPrefChanged(
"foo",
true);
Assert.ok(initFeedStub.calledWith(
"foo"));
Assert.ok(!uninitFeedStub.calledOnce);
initFeedStub.resetHistory();
uninitFeedStub.resetHistory();
info(
"onPrefChanged should uninitialize the feed if called with false");
store.onPrefChanged(
"foo",
false);
Assert.ok(uninitFeedStub.calledWith(
"foo"));
Assert.ok(!initFeedStub.calledOnce);
initFeedStub.resetHistory();
uninitFeedStub.resetHistory();
info(
"onPrefChanged should do nothing if not an expected feed");
store.onPrefChanged(
"bar",
false);
Assert.ok(!initFeedStub.calledOnce);
Assert.ok(!uninitFeedStub.calledOnce);
sandbox.restore();
});
add_task(async
function test_uninit() {
let sandbox = sinon.createSandbox();
let store =
new Store();
let dispatchStub = sandbox.stub(store,
"dispatch");
const action = { type:
"BAR" };
await store.init(
new Map(),
null, action);
store.uninit();
Assert.ok(store.dispatch.calledOnce);
Assert.ok(store.dispatch.calledWith(action));
info(
"Store.uninit should clear .feeds and ._feedFactories");
store._prefs.set(
"a",
true);
await store.init(
new Map([
[
"a", () => ({})],
[
"b", () => ({})],
[
"c", () => ({})],
])
);
store.uninit();
Assert.equal(store.feeds.size, 0);
Assert.equal(store._feedFactories,
null);
info(
"Store.uninit should emit an uninit event if provided on init");
dispatchStub.resetHistory();
const uninitAction = { type:
"BAR" };
await store.init(
new Map(),
null, uninitAction);
store.uninit();
Assert.ok(store.dispatch.calledOnce);
Assert.ok(store.dispatch.calledWith(uninitAction));
sandbox.restore();
});
add_task(async
function test_getState() {
info(
"Store.getState should return the redux state");
let sandbox = sinon.createSandbox();
let store =
new Store();
store._store = Redux.createStore((prevState = 123) => prevState);
const { getState } = store;
Assert.equal(getState(), 123);
sandbox.restore();
});
/**
* addNumberReducer - a simple dummy reducer for testing that adds a number
*/
function addNumberReducer(prevState = 0, action) {
return action.type ===
"ADD" ? prevState + action.data : prevState;
}
add_task(async
function test_dispatch() {
info(
"Store.dispatch should call .onAction of each feed");
let sandbox = sinon.createSandbox();
let store =
new Store();
const { dispatch } = store;
const sub = { onAction: sinon.spy() };
const action = { type:
"FOO" };
store._prefs.set(
"sub",
true);
await store.init(
new Map([[
"sub", () => sub]]));
dispatch(action);
Assert.ok(sub.onAction.calledWith(action));
info(
"Sandbox.dispatch should call the reducers");
store._store = Redux.createStore(addNumberReducer);
dispatch({ type:
"ADD", data: 14 });
Assert.equal(store.getState(), 14);
sandbox.restore();
});
add_task(async
function test_subscribe() {
info(
"Store.subscribe should subscribe to changes to the store");
let sandbox = sinon.createSandbox();
let store =
new Store();
const sub = sandbox.spy();
const action = { type:
"FOO" };
store.subscribe(sub);
store.dispatch(action);
Assert.ok(sub.calledOnce);
sandbox.restore();
});