test_Store.js (11433B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 ChromeUtils.defineESModuleGetters(this, { 7 ActivityStreamMessageChannel: 8 "resource://newtab/lib/ActivityStreamMessageChannel.sys.mjs", 9 sinon: "resource://testing-common/Sinon.sys.mjs", 10 Store: "resource://newtab/lib/Store.sys.mjs", 11 }); 12 13 // This creates the Redux top-level object. 14 /* globals Redux */ 15 Services.scriptloader.loadSubScript( 16 "chrome://global/content/vendor/redux.js", 17 this 18 ); 19 20 add_task(async function test_expected_properties() { 21 let sandbox = sinon.createSandbox(); 22 let store = new Store(); 23 24 Assert.equal(store.feeds.constructor.name, "Map", "Should create a Map"); 25 Assert.equal(store.feeds.size, 0, "Store should start without any feeds."); 26 27 Assert.ok(store._store, "Has a ._store"); 28 Assert.ok(store.dispatch, "Has a .dispatch"); 29 Assert.ok(store.getState, "Has a .getState"); 30 31 sandbox.restore(); 32 }); 33 34 add_task(async function test_messagechannel() { 35 let sandbox = sinon.createSandbox(); 36 sandbox 37 .stub(ActivityStreamMessageChannel.prototype, "middleware") 38 .returns(() => next => action => next(action)); 39 let store = new Store(); 40 41 info( 42 "Store should create a ActivityStreamMessageChannel with the right dispatcher" 43 ); 44 Assert.ok(store.getMessageChannel(), "Has a message channel"); 45 Assert.equal( 46 store.getMessageChannel().dispatch, 47 store.dispatch, 48 "MessageChannel.dispatch forwards to store.dispatch" 49 ); 50 Assert.equal( 51 store.getMessageChannel(), 52 store._messageChannel, 53 "_messageChannel is the member for getMessageChannel()" 54 ); 55 56 store.dispatch({ type: "FOO" }); 57 Assert.ok( 58 ActivityStreamMessageChannel.prototype.middleware.calledOnce, 59 "Middleware called." 60 ); 61 sandbox.restore(); 62 }); 63 64 add_task(async function test_initFeed_add_feeds() { 65 info("Store.initFeed should add an instance of the feed to .feeds"); 66 67 let sandbox = sinon.createSandbox(); 68 let store = new Store(); 69 class Foo {} 70 store._prefs.set("foo", true); 71 await store.init(new Map([["foo", () => new Foo()]])); 72 store.initFeed("foo"); 73 74 Assert.ok(store.feeds.has("foo"), "foo is set"); 75 Assert.ok(store.feeds.get("foo") instanceof Foo, "Got registered class"); 76 sandbox.restore(); 77 }); 78 79 add_task(async function test_initFeed_calls_onAction() { 80 info("Store should call the feed's onAction with uninit action if it exists"); 81 82 let sandbox = sinon.createSandbox(); 83 let store = new Store(); 84 let testFeed; 85 let createTestFeed = () => { 86 testFeed = { onAction: sandbox.spy() }; 87 return testFeed; 88 }; 89 const action = { type: "FOO" }; 90 store._feedFactories = new Map([["test", createTestFeed]]); 91 92 store.initFeed("test", action); 93 94 Assert.ok(testFeed.onAction.calledOnce, "onAction called"); 95 Assert.ok( 96 testFeed.onAction.calledWith(action), 97 "onAction called with test action" 98 ); 99 100 info("Store should add a .store property to the feed"); 101 Assert.ok(testFeed.store, "Store exists"); 102 Assert.equal(testFeed.store, store, "Feed store is the Store"); 103 sandbox.restore(); 104 }); 105 106 add_task(async function test_initFeed_on_init() { 107 info("Store should call .initFeed with each key"); 108 109 let sandbox = sinon.createSandbox(); 110 let store = new Store(); 111 112 sandbox.stub(store, "initFeed"); 113 store._prefs.set("foo", true); 114 store._prefs.set("bar", true); 115 await store.init( 116 new Map([ 117 ["foo", () => {}], 118 ["bar", () => {}], 119 ]) 120 ); 121 Assert.ok(store.initFeed.calledWith("foo"), "First test feed initted"); 122 Assert.ok(store.initFeed.calledWith("bar"), "Second test feed initted"); 123 sandbox.restore(); 124 }); 125 126 add_task(async function test_disabled_feed() { 127 info("Store should not initialize the feed if the Pref is set to false"); 128 129 let sandbox = sinon.createSandbox(); 130 let store = new Store(); 131 132 sandbox.stub(store, "initFeed"); 133 store._prefs.set("foo", false); 134 await store.init(new Map([["foo", () => {}]])); 135 Assert.ok(store.initFeed.notCalled, ".initFeed not called"); 136 137 store._prefs.set("foo", true); 138 139 sandbox.restore(); 140 }); 141 142 add_task(async function test_observe_pref_branch() { 143 info("Store should observe the pref branch"); 144 145 let sandbox = sinon.createSandbox(); 146 let store = new Store(); 147 148 sandbox.stub(store._prefs, "observeBranch"); 149 await store.init(new Map()); 150 Assert.ok(store._prefs.observeBranch.calledOnce, "observeBranch called once"); 151 Assert.ok( 152 store._prefs.observeBranch.calledWith(store), 153 "observeBranch passed the store" 154 ); 155 156 sandbox.restore(); 157 }); 158 159 add_task(async function test_emit_initial_event() { 160 info("Store should emit an initial event if provided"); 161 162 let sandbox = sinon.createSandbox(); 163 let store = new Store(); 164 165 const action = { type: "FOO" }; 166 sandbox.stub(store, "dispatch"); 167 await store.init(new Map(), action); 168 Assert.ok(store.dispatch.calledOnce, "Dispatch called once"); 169 Assert.ok(store.dispatch.calledWith(action), "Dispatch called with action"); 170 171 sandbox.restore(); 172 }); 173 174 add_task(async function test_initialize_telemetry_feed_first() { 175 info("Store should initialize the telemetry feed first"); 176 177 let sandbox = sinon.createSandbox(); 178 let store = new Store(); 179 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([ 186 ["feeds.foo", fooSpy], 187 ["feeds.telemetry", telemetrySpy], 188 ]); 189 await store.init(feedFactories); 190 Assert.ok(telemetrySpy.calledBefore(fooSpy), "Telemetry feed initted first"); 191 192 sandbox.restore(); 193 }); 194 195 add_task(async function test_dispatch_init_load_events() { 196 info("Store should dispatch init/load events"); 197 198 let sandbox = sinon.createSandbox(); 199 let store = new Store(); 200 201 sandbox.stub(store.getMessageChannel(), "simulateMessagesForExistingTabs"); 202 await store.init(new Map(), { type: "FOO" }); 203 Assert.ok( 204 store.getMessageChannel().simulateMessagesForExistingTabs.calledOnce, 205 "simulateMessagesForExistingTabs called once" 206 ); 207 208 sandbox.restore(); 209 }); 210 211 add_task(async function test_init_before_load() { 212 info("Store should dispatch INIT before LOAD"); 213 214 let sandbox = sinon.createSandbox(); 215 let store = new Store(); 216 217 sandbox.stub(store.getMessageChannel(), "simulateMessagesForExistingTabs"); 218 sandbox.stub(store, "dispatch"); 219 const init = { type: "INIT" }; 220 const load = { type: "TAB_LOAD" }; 221 store 222 .getMessageChannel() 223 .simulateMessagesForExistingTabs.callsFake(() => store.dispatch(load)); 224 await store.init(new Map(), init); 225 226 Assert.ok(store.dispatch.calledTwice, "Dispatch called twice"); 227 Assert.equal( 228 store.dispatch.firstCall.args[0], 229 init, 230 "First dispatch was for init event" 231 ); 232 Assert.equal( 233 store.dispatch.secondCall.args[0], 234 load, 235 "Second dispatch was for load event" 236 ); 237 238 sandbox.restore(); 239 }); 240 241 add_task(async function test_uninit_feeds() { 242 info("uninitFeed should not throw if no feed with that name exists"); 243 244 let sandbox = sinon.createSandbox(); 245 let store = new Store(); 246 247 try { 248 store.uninitFeed("does-not-exist"); 249 Assert.ok(true, "Didn't throw"); 250 } catch (e) { 251 Assert.ok(false, "Should not have thrown"); 252 } 253 254 info( 255 "uninitFeed should call the feed's onAction with uninit action if it exists" 256 ); 257 let feed; 258 function createFeed() { 259 feed = { onAction: sandbox.spy() }; 260 return feed; 261 } 262 const action = { type: "BAR" }; 263 store._feedFactories = new Map([["foo", createFeed]]); 264 store.initFeed("foo"); 265 266 store.uninitFeed("foo", action); 267 268 Assert.ok(feed.onAction.calledOnce); 269 Assert.ok(feed.onAction.calledWith(action)); 270 271 info("uninitFeed should remove the feed from .feeds"); 272 Assert.ok(!store.feeds.has("foo"), "foo is not in .feeds"); 273 274 sandbox.restore(); 275 }); 276 277 add_task(async function test_onPrefChanged() { 278 let sandbox = sinon.createSandbox(); 279 let store = new Store(); 280 let initFeedStub = sandbox.stub(store, "initFeed"); 281 let uninitFeedStub = sandbox.stub(store, "uninitFeed"); 282 store._prefs.set("foo", false); 283 store.init(new Map([["foo", () => ({})]])); 284 285 info("onPrefChanged should initialize the feed if called with true"); 286 store.onPrefChanged("foo", true); 287 Assert.ok(initFeedStub.calledWith("foo")); 288 Assert.ok(!uninitFeedStub.calledOnce); 289 initFeedStub.resetHistory(); 290 uninitFeedStub.resetHistory(); 291 292 info("onPrefChanged should uninitialize the feed if called with false"); 293 store.onPrefChanged("foo", false); 294 Assert.ok(uninitFeedStub.calledWith("foo")); 295 Assert.ok(!initFeedStub.calledOnce); 296 initFeedStub.resetHistory(); 297 uninitFeedStub.resetHistory(); 298 299 info("onPrefChanged should do nothing if not an expected feed"); 300 store.onPrefChanged("bar", false); 301 302 Assert.ok(!initFeedStub.calledOnce); 303 Assert.ok(!uninitFeedStub.calledOnce); 304 sandbox.restore(); 305 }); 306 307 add_task(async function test_uninit() { 308 let sandbox = sinon.createSandbox(); 309 let store = new Store(); 310 let dispatchStub = sandbox.stub(store, "dispatch"); 311 const action = { type: "BAR" }; 312 await store.init(new Map(), null, action); 313 store.uninit(); 314 315 Assert.ok(store.dispatch.calledOnce); 316 Assert.ok(store.dispatch.calledWith(action)); 317 318 info("Store.uninit should clear .feeds and ._feedFactories"); 319 store._prefs.set("a", true); 320 await store.init( 321 new Map([ 322 ["a", () => ({})], 323 ["b", () => ({})], 324 ["c", () => ({})], 325 ]) 326 ); 327 328 store.uninit(); 329 330 Assert.equal(store.feeds.size, 0); 331 Assert.equal(store._feedFactories, null); 332 333 info("Store.uninit should emit an uninit event if provided on init"); 334 dispatchStub.resetHistory(); 335 const uninitAction = { type: "BAR" }; 336 await store.init(new Map(), null, uninitAction); 337 store.uninit(); 338 339 Assert.ok(store.dispatch.calledOnce); 340 Assert.ok(store.dispatch.calledWith(uninitAction)); 341 sandbox.restore(); 342 }); 343 344 add_task(async function test_getState() { 345 info("Store.getState should return the redux state"); 346 let sandbox = sinon.createSandbox(); 347 let store = new Store(); 348 store._store = Redux.createStore((prevState = 123) => prevState); 349 const { getState } = store; 350 Assert.equal(getState(), 123); 351 sandbox.restore(); 352 }); 353 354 /** 355 * addNumberReducer - a simple dummy reducer for testing that adds a number 356 */ 357 function addNumberReducer(prevState = 0, action) { 358 return action.type === "ADD" ? prevState + action.data : prevState; 359 } 360 361 add_task(async function test_dispatch() { 362 info("Store.dispatch should call .onAction of each feed"); 363 let sandbox = sinon.createSandbox(); 364 let store = new Store(); 365 const { dispatch } = store; 366 const sub = { onAction: sinon.spy() }; 367 const action = { type: "FOO" }; 368 369 store._prefs.set("sub", true); 370 await store.init(new Map([["sub", () => sub]])); 371 372 dispatch(action); 373 374 Assert.ok(sub.onAction.calledWith(action)); 375 376 info("Sandbox.dispatch should call the reducers"); 377 378 store._store = Redux.createStore(addNumberReducer); 379 dispatch({ type: "ADD", data: 14 }); 380 Assert.equal(store.getState(), 14); 381 382 sandbox.restore(); 383 }); 384 385 add_task(async function test_subscribe() { 386 info("Store.subscribe should subscribe to changes to the store"); 387 let sandbox = sinon.createSandbox(); 388 let store = new Store(); 389 const sub = sandbox.spy(); 390 const action = { type: "FOO" }; 391 392 store.subscribe(sub); 393 store.dispatch(action); 394 395 Assert.ok(sub.calledOnce); 396 sandbox.restore(); 397 });