summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/xpcshell/test_Store.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/xpcshell/test_Store.js')
-rw-r--r--browser/components/newtab/test/xpcshell/test_Store.js453
1 files changed, 453 insertions, 0 deletions
diff --git a/browser/components/newtab/test/xpcshell/test_Store.js b/browser/components/newtab/test/xpcshell/test_Store.js
new file mode 100644
index 0000000000..b05ad36cd6
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_Store.js
@@ -0,0 +1,453 @@
+/* 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 { ActivityStreamStorage } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// This creates the Redux top-level object.
+/* globals Redux */
+Services.scriptloader.loadSubScript(
+ "resource://activity-stream/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(s => 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_initFeed_calls__initIndexedDB() {
+ info("Store should call _initIndexedDB");
+ let sandbox = sinon.createSandbox();
+ let store = new Store();
+
+ sandbox.spy(store, "_initIndexedDB");
+
+ let dbStub = sandbox.stub(ActivityStreamStorage.prototype, "db");
+ let dbAccessed = false;
+ dbStub.get(() => {
+ dbAccessed = true;
+ return {};
+ });
+
+ store._prefs.set("testfeed", true);
+ await store.init(
+ new Map([
+ [
+ "testfeed",
+ () => {
+ return {};
+ },
+ ],
+ ])
+ );
+
+ Assert.ok(store._initIndexedDB.calledOnce, "_initIndexedDB called once");
+ Assert.ok(
+ store._initIndexedDB.calledWithExactly("feeds.telemetry"),
+ "feeds.telemetry was passed"
+ );
+ // Due to what appears to be a bug in sinon when using calledOnce
+ // with a stubbed getter, we can't just use dbStub.calledOnce here.
+ Assert.ok(dbAccessed, "ActivityStreamStorage was accessed");
+
+ info(
+ "Store should reset ActivityStreamStorage telemetry if opening the db fails"
+ );
+ dbStub.rejects();
+ await store.init(new Map());
+
+ Assert.equal(
+ store.dbStorage.telemetry,
+ null,
+ "Telemetry on storage was cleared"
+ );
+
+ 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();
+});