Store.test.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import {addNumberReducer, FakePrefs} from "test/unit/utils";
  2. import {createStore} from "redux";
  3. import injector from "inject!lib/Store.jsm";
  4. describe("Store", () => {
  5. let Store;
  6. let sandbox;
  7. let store;
  8. let dbStub;
  9. beforeEach(() => {
  10. sandbox = sinon.createSandbox();
  11. function ActivityStreamMessageChannel(options) {
  12. this.dispatch = options.dispatch;
  13. this.createChannel = sandbox.spy();
  14. this.destroyChannel = sandbox.spy();
  15. this.middleware = sandbox.spy(s => next => action => next(action));
  16. this.simulateMessagesForExistingTabs = sandbox.stub();
  17. }
  18. dbStub = sandbox.stub().resolves();
  19. function FakeActivityStreamStorage() {
  20. this.db = {};
  21. sinon.stub(this, "db").get(dbStub);
  22. }
  23. ({Store} = injector({
  24. "lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel},
  25. "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs},
  26. "lib/ActivityStreamStorage.jsm": {ActivityStreamStorage: FakeActivityStreamStorage},
  27. }));
  28. store = new Store();
  29. sandbox.stub(store, "_initIndexedDB").resolves();
  30. });
  31. afterEach(() => {
  32. sandbox.restore();
  33. });
  34. it("should have a .feeds property that is a Map", () => {
  35. assert.instanceOf(store.feeds, Map);
  36. assert.equal(store.feeds.size, 0, ".feeds.size");
  37. });
  38. it("should have a redux store at ._store", () => {
  39. assert.ok(store._store);
  40. assert.property(store, "dispatch");
  41. assert.property(store, "getState");
  42. });
  43. it("should create a ActivityStreamMessageChannel with the right dispatcher", () => {
  44. assert.ok(store._messageChannel);
  45. assert.equal(store._messageChannel.dispatch, store.dispatch);
  46. });
  47. it("should connect the ActivityStreamMessageChannel's middleware", () => {
  48. store.dispatch({type: "FOO"});
  49. assert.calledOnce(store._messageChannel.middleware);
  50. });
  51. describe("#initFeed", () => {
  52. it("should add an instance of the feed to .feeds", () => {
  53. class Foo {}
  54. store._prefs.set("foo", true);
  55. store.init(new Map([["foo", () => new Foo()]]));
  56. store.initFeed("foo");
  57. assert.isTrue(store.feeds.has("foo"), "foo is set");
  58. assert.instanceOf(store.feeds.get("foo"), Foo);
  59. });
  60. it("should call the feed's onAction with uninit action if it exists", () => {
  61. let feed;
  62. function createFeed() {
  63. feed = {onAction: sinon.spy()};
  64. return feed;
  65. }
  66. const action = {type: "FOO"};
  67. store._feedFactories = new Map([["foo", createFeed]]);
  68. store.initFeed("foo", action);
  69. assert.calledOnce(feed.onAction);
  70. assert.calledWith(feed.onAction, action);
  71. });
  72. it("should add a .store property to the feed", () => {
  73. class Foo {}
  74. store._feedFactories = new Map([["foo", () => new Foo()]]);
  75. store.initFeed("foo");
  76. assert.propertyVal(store.feeds.get("foo"), "store", store);
  77. });
  78. });
  79. describe("#uninitFeed", () => {
  80. it("should not throw if no feed with that name exists", () => {
  81. assert.doesNotThrow(() => {
  82. store.uninitFeed("bar");
  83. });
  84. });
  85. it("should call the feed's onAction with uninit action if it exists", () => {
  86. let feed;
  87. function createFeed() {
  88. feed = {onAction: sinon.spy()};
  89. return feed;
  90. }
  91. const action = {type: "BAR"};
  92. store._feedFactories = new Map([["foo", createFeed]]);
  93. store.initFeed("foo");
  94. store.uninitFeed("foo", action);
  95. assert.calledOnce(feed.onAction);
  96. assert.calledWith(feed.onAction, action);
  97. });
  98. it("should remove the feed from .feeds", () => {
  99. class Foo {}
  100. store._feedFactories = new Map([["foo", () => new Foo()]]);
  101. store.initFeed("foo");
  102. store.uninitFeed("foo");
  103. assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
  104. });
  105. });
  106. describe("onPrefChanged", () => {
  107. beforeEach(() => {
  108. sinon.stub(store, "initFeed");
  109. sinon.stub(store, "uninitFeed");
  110. store._prefs.set("foo", false);
  111. store.init(new Map([["foo", () => ({})]]));
  112. });
  113. it("should initialize the feed if called with true", () => {
  114. store.onPrefChanged("foo", true);
  115. assert.calledWith(store.initFeed, "foo");
  116. assert.notCalled(store.uninitFeed);
  117. });
  118. it("should uninitialize the feed if called with false", () => {
  119. store.onPrefChanged("foo", false);
  120. assert.calledWith(store.uninitFeed, "foo");
  121. assert.notCalled(store.initFeed);
  122. });
  123. it("should do nothing if not an expected feed", () => {
  124. store.onPrefChanged("bar", false);
  125. assert.notCalled(store.initFeed);
  126. assert.notCalled(store.uninitFeed);
  127. });
  128. });
  129. describe("#init", () => {
  130. it("should call .initFeed with each key", async () => {
  131. sinon.stub(store, "initFeed");
  132. store._prefs.set("foo", true);
  133. store._prefs.set("bar", true);
  134. await store.init(new Map([["foo", () => {}], ["bar", () => {}]]));
  135. assert.calledWith(store.initFeed, "foo");
  136. assert.calledWith(store.initFeed, "bar");
  137. });
  138. it("should call _initIndexedDB", async () => {
  139. await store.init(new Map());
  140. assert.calledOnce(store._initIndexedDB);
  141. assert.calledWithExactly(store._initIndexedDB, "feeds.telemetry");
  142. });
  143. it("should access the db property of indexedDB", async () => {
  144. store._initIndexedDB.restore();
  145. await store.init(new Map());
  146. assert.calledOnce(dbStub);
  147. });
  148. it("should reset ActivityStreamStorage telemetry if opening the db fails", async () => {
  149. store._initIndexedDB.restore();
  150. // Force an IndexedDB error
  151. dbStub.rejects();
  152. await store.init(new Map());
  153. assert.calledOnce(dbStub);
  154. assert.isNull(store.dbStorage.telemetry);
  155. });
  156. it("should not initialize the feed if the Pref is set to false", async () => {
  157. sinon.stub(store, "initFeed");
  158. store._prefs.set("foo", false);
  159. await store.init(new Map([["foo", () => {}]]));
  160. assert.notCalled(store.initFeed);
  161. });
  162. it("should observe the pref branch", async () => {
  163. sinon.stub(store._prefs, "observeBranch");
  164. await store.init(new Map());
  165. assert.calledOnce(store._prefs.observeBranch);
  166. assert.calledWith(store._prefs.observeBranch, store);
  167. });
  168. it("should initialize the ActivityStreamMessageChannel channel", async () => {
  169. await store.init(new Map());
  170. assert.calledOnce(store._messageChannel.createChannel);
  171. });
  172. it("should emit an initial event if provided", async () => {
  173. sinon.stub(store, "dispatch");
  174. const action = {type: "FOO"};
  175. await store.init(new Map(), action);
  176. assert.calledOnce(store.dispatch);
  177. assert.calledWith(store.dispatch, action);
  178. });
  179. it("should initialize the telemtry feed first", () => {
  180. store._prefs.set("feeds.foo", true);
  181. store._prefs.set("feeds.telemetry", true);
  182. const telemetrySpy = sandbox.stub().returns({});
  183. const fooSpy = sandbox.stub().returns({});
  184. // Intentionally put the telemetry feed as the second item.
  185. const feedFactories = new Map([["feeds.foo", fooSpy],
  186. ["feeds.telemetry", telemetrySpy]]);
  187. store.init(feedFactories);
  188. assert.ok(telemetrySpy.calledBefore(fooSpy));
  189. });
  190. it("should dispatch init/load events", async () => {
  191. await store.init(new Map(), {type: "FOO"});
  192. assert.calledOnce(store._messageChannel.simulateMessagesForExistingTabs);
  193. });
  194. it("should dispatch INIT before LOAD", async () => {
  195. const init = {type: "INIT"};
  196. const load = {type: "TAB_LOAD"};
  197. sandbox.stub(store, "dispatch");
  198. store._messageChannel.simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load));
  199. await store.init(new Map(), init);
  200. assert.calledTwice(store.dispatch);
  201. assert.equal(store.dispatch.firstCall.args[0], init);
  202. assert.equal(store.dispatch.secondCall.args[0], load);
  203. });
  204. });
  205. describe("#uninit", () => {
  206. it("should emit an uninit event if provided on init", () => {
  207. sinon.stub(store, "dispatch");
  208. const action = {type: "BAR"};
  209. store.init(new Map(), null, action);
  210. store.uninit();
  211. assert.calledOnce(store.dispatch);
  212. assert.calledWith(store.dispatch, action);
  213. });
  214. it("should clear .feeds and ._feedFactories", () => {
  215. store._prefs.set("a", true);
  216. store.init(new Map([
  217. ["a", () => ({})],
  218. ["b", () => ({})],
  219. ["c", () => ({})],
  220. ]));
  221. store.uninit();
  222. assert.equal(store.feeds.size, 0);
  223. assert.isNull(store._feedFactories);
  224. });
  225. it("should destroy the ActivityStreamMessageChannel channel", () => {
  226. store.uninit();
  227. assert.calledOnce(store._messageChannel.destroyChannel);
  228. });
  229. });
  230. describe("#getState", () => {
  231. it("should return the redux state", () => {
  232. store._store = createStore((prevState = 123) => prevState);
  233. const {getState} = store;
  234. assert.equal(getState(), 123);
  235. });
  236. });
  237. describe("#dispatch", () => {
  238. it("should call .onAction of each feed", async () => {
  239. const {dispatch} = store;
  240. const sub = {onAction: sinon.spy()};
  241. const action = {type: "FOO"};
  242. store._prefs.set("sub", true);
  243. await store.init(new Map([["sub", () => sub]]));
  244. dispatch(action);
  245. assert.calledWith(sub.onAction, action);
  246. });
  247. it("should call the reducers", () => {
  248. const {dispatch} = store;
  249. store._store = createStore(addNumberReducer);
  250. dispatch({type: "ADD", data: 14});
  251. assert.equal(store.getState(), 14);
  252. });
  253. });
  254. describe("#subscribe", () => {
  255. it("should subscribe to changes to the store", () => {
  256. const sub = sinon.spy();
  257. const action = {type: "FOO"};
  258. store.subscribe(sub);
  259. store.dispatch(action);
  260. assert.calledOnce(sub);
  261. });
  262. });
  263. });