ActivityStreamMessageChannel.test.js (15102B)
1 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; 2 import { 3 ActivityStreamMessageChannel, 4 DEFAULT_OPTIONS, 5 } from "lib/ActivityStreamMessageChannel.sys.mjs"; 6 import { addNumberReducer, GlobalOverrider } from "test/unit/utils"; 7 import { applyMiddleware, createStore } from "redux"; 8 9 const OPTIONS = [ 10 "pageURL, outgoingMessageName", 11 "incomingMessageName", 12 "dispatch", 13 ]; 14 15 // Create an object containing details about a tab as expected within 16 // the loaded tabs map in ActivityStreamMessageChannel.sys.mjs. 17 function getTabDetails(portID, url = "about:newtab", extraArgs = {}) { 18 let actor = { 19 portID, 20 sendAsyncMessage: sinon.spy(), 21 }; 22 let browser = { 23 getAttribute: () => (extraArgs.preloaded ? "preloaded" : ""), 24 ownerGlobal: {}, 25 }; 26 let browsingContext = { 27 top: { 28 embedderElement: browser, 29 }, 30 }; 31 32 let data = { 33 data: { 34 actor, 35 browser, 36 browsingContext, 37 portID, 38 url, 39 }, 40 target: { 41 browsingContext, 42 }, 43 }; 44 45 if (extraArgs.loaded) { 46 data.data.loaded = extraArgs.loaded; 47 } 48 if (extraArgs.simulated) { 49 data.data.simulated = extraArgs.simulated; 50 } 51 52 return data; 53 } 54 55 describe("ActivityStreamMessageChannel", () => { 56 let globals; 57 let dispatch; 58 let mm; 59 beforeEach(() => { 60 globals = new GlobalOverrider(); 61 globals.set("AboutNewTab", { 62 reset: globals.sandbox.spy(), 63 }); 64 globals.set("AboutHomeStartupCache", { onPreloadedNewTabMessage() {} }); 65 globals.set("AboutNewTabParent", { 66 flushQueuedMessagesFromContent: globals.sandbox.stub(), 67 }); 68 69 dispatch = globals.sandbox.spy(); 70 mm = new ActivityStreamMessageChannel({ dispatch }); 71 72 assert.ok(mm.loadedTabs, []); 73 74 let loadedTabs = new Map(); 75 let sandbox = sinon.createSandbox(); 76 sandbox.stub(mm, "loadedTabs").get(() => loadedTabs); 77 }); 78 79 afterEach(() => globals.restore()); 80 81 describe("portID validation", () => { 82 let sandbox; 83 beforeEach(() => { 84 sandbox = sinon.createSandbox(); 85 sandbox.spy(global.console, "error"); 86 }); 87 afterEach(() => { 88 sandbox.restore(); 89 }); 90 it("should log errors for an invalid portID", () => { 91 mm.validatePortID({}); 92 mm.validatePortID({}); 93 mm.validatePortID({}); 94 95 assert.equal(global.console.error.callCount, 3); 96 }); 97 }); 98 99 it("should exist", () => { 100 assert.ok(ActivityStreamMessageChannel); 101 }); 102 it("should apply default options", () => { 103 mm = new ActivityStreamMessageChannel(); 104 OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o)); 105 }); 106 it("should add options", () => { 107 const options = { 108 dispatch: () => {}, 109 pageURL: "FOO.html", 110 outgoingMessageName: "OUT", 111 incomingMessageName: "IN", 112 }; 113 mm = new ActivityStreamMessageChannel(options); 114 OPTIONS.forEach(o => assert.equal(mm[o], options[o], o)); 115 }); 116 it("should throw an error if no dispatcher was provided", () => { 117 mm = new ActivityStreamMessageChannel(); 118 assert.throws(() => mm.dispatch({ type: "FOO" })); 119 }); 120 describe("Creating/destroying the channel", () => { 121 describe("#simulateMessagesForExistingTabs", () => { 122 beforeEach(() => { 123 sinon.stub(mm, "onActionFromContent"); 124 }); 125 it("should simulate init for existing ports", () => { 126 let msg1 = getTabDetails("inited", "about:monkeys", { 127 simulated: true, 128 }); 129 mm.loadedTabs.set(msg1.data.browser, msg1.data); 130 131 let msg2 = getTabDetails("loaded", "about:sheep", { 132 simulated: true, 133 }); 134 mm.loadedTabs.set(msg2.data.browser, msg2.data); 135 136 mm.simulateMessagesForExistingTabs(); 137 138 assert.calledWith(mm.onActionFromContent.firstCall, { 139 type: at.NEW_TAB_INIT, 140 data: msg1.data, 141 }); 142 assert.calledWith(mm.onActionFromContent.secondCall, { 143 type: at.NEW_TAB_INIT, 144 data: msg2.data, 145 }); 146 }); 147 it("should simulate load for loaded ports", () => { 148 let msg3 = getTabDetails("foo", null, { 149 preloaded: true, 150 loaded: true, 151 }); 152 mm.loadedTabs.set(msg3.data.browser, msg3.data); 153 154 mm.simulateMessagesForExistingTabs(); 155 156 assert.calledWith( 157 mm.onActionFromContent, 158 { type: at.NEW_TAB_LOAD }, 159 "foo" 160 ); 161 }); 162 it("should set renderLayers on preloaded browsers after load", () => { 163 let msg4 = getTabDetails("foo", null, { 164 preloaded: true, 165 loaded: true, 166 }); 167 msg4.data.browser.ownerGlobal = { 168 STATE_MAXIMIZED: 1, 169 STATE_MINIMIZED: 2, 170 STATE_NORMAL: 3, 171 STATE_FULLSCREEN: 4, 172 windowState: 3, 173 isFullyOccluded: false, 174 }; 175 mm.loadedTabs.set(msg4.data.browser, msg4.data); 176 mm.simulateMessagesForExistingTabs(); 177 assert.equal(msg4.data.browser.renderLayers, true); 178 }); 179 it("should flush queued messages from content when doing the simulation", () => { 180 assert.notCalled( 181 global.AboutNewTabParent.flushQueuedMessagesFromContent 182 ); 183 mm.simulateMessagesForExistingTabs(); 184 assert.calledOnce( 185 global.AboutNewTabParent.flushQueuedMessagesFromContent 186 ); 187 }); 188 }); 189 }); 190 describe("Message handling", () => { 191 describe("#getTargetById", () => { 192 it("should get an id if it exists", () => { 193 let msg = getTabDetails("foo:1"); 194 mm.loadedTabs.set(msg.data.browser, msg.data); 195 assert.equal(mm.getTargetById("foo:1"), msg.data.actor); 196 }); 197 it("should return null if the target doesn't exist", () => { 198 let msg = getTabDetails("foo:2"); 199 mm.loadedTabs.set(msg.data.browser, msg.data); 200 assert.equal(mm.getTargetById("bar:3"), null); 201 }); 202 }); 203 describe("#getPreloadedActors", () => { 204 it("should get a preloaded actor if it exists", () => { 205 let msg = getTabDetails("foo:3", null, { preloaded: true }); 206 mm.loadedTabs.set(msg.data.browser, msg.data); 207 assert.equal(mm.getPreloadedActors()[0].portID, "foo:3"); 208 }); 209 it("should get all the preloaded actors across windows if they exist", () => { 210 let msg = getTabDetails("foo:4a", null, { preloaded: true }); 211 mm.loadedTabs.set(msg.data.browser, msg.data); 212 msg = getTabDetails("foo:4b", null, { preloaded: true }); 213 mm.loadedTabs.set(msg.data.browser, msg.data); 214 assert.equal(mm.getPreloadedActors().length, 2); 215 }); 216 it("should return null if there is no preloaded actor", () => { 217 let msg = getTabDetails("foo:5"); 218 mm.loadedTabs.set(msg.data.browser, msg.data); 219 assert.equal(mm.getPreloadedActors(), null); 220 }); 221 }); 222 describe("#onNewTabInit", () => { 223 it("should dispatch a NEW_TAB_INIT action", () => { 224 let msg = getTabDetails("foo", "about:monkeys"); 225 sinon.stub(mm, "onActionFromContent"); 226 227 mm.onNewTabInit(msg, msg.data); 228 229 assert.calledWith(mm.onActionFromContent, { 230 type: at.NEW_TAB_INIT, 231 data: msg.data, 232 }); 233 }); 234 }); 235 describe("#onNewTabLoad", () => { 236 it("should dispatch a NEW_TAB_LOAD action", () => { 237 let msg = getTabDetails("foo", null, { preloaded: true }); 238 mm.loadedTabs.set(msg.data.browser, msg.data); 239 sinon.stub(mm, "onActionFromContent"); 240 mm.onNewTabLoad({ target: msg.target }, msg.data); 241 assert.calledWith( 242 mm.onActionFromContent, 243 { type: at.NEW_TAB_LOAD }, 244 "foo" 245 ); 246 }); 247 }); 248 describe("#onNewTabUnload", () => { 249 it("should dispatch a NEW_TAB_UNLOAD action", () => { 250 let msg = getTabDetails("foo"); 251 mm.loadedTabs.set(msg.data.browser, msg.data); 252 sinon.stub(mm, "onActionFromContent"); 253 mm.onNewTabUnload({ target: msg.target }, msg.data); 254 assert.calledWith( 255 mm.onActionFromContent, 256 { type: at.NEW_TAB_UNLOAD }, 257 "foo" 258 ); 259 }); 260 }); 261 describe("#onMessage", () => { 262 let sandbox; 263 beforeEach(() => { 264 sandbox = sinon.createSandbox(); 265 sandbox.spy(global.console, "error"); 266 }); 267 afterEach(() => sandbox.restore()); 268 it("return early when tab details are not present", () => { 269 let msg = getTabDetails("foo"); 270 sinon.stub(mm, "onActionFromContent"); 271 mm.onMessage(msg, msg.data); 272 assert.notCalled(mm.onActionFromContent); 273 }); 274 it("should report an error if the msg.data is missing", () => { 275 let msg = getTabDetails("foo"); 276 mm.loadedTabs.set(msg.data.browser, msg.data); 277 let tabDetails = msg.data; 278 delete msg.data; 279 mm.onMessage(msg, tabDetails); 280 assert.calledOnce(global.console.error); 281 }); 282 it("should report an error if the msg.data.type is missing", () => { 283 let msg = getTabDetails("foo"); 284 mm.loadedTabs.set(msg.data.browser, msg.data); 285 msg.data = "foo"; 286 mm.onMessage(msg, msg.data); 287 assert.calledOnce(global.console.error); 288 }); 289 it("should call onActionFromContent", () => { 290 sinon.stub(mm, "onActionFromContent"); 291 let msg = getTabDetails("foo"); 292 mm.loadedTabs.set(msg.data.browser, msg.data); 293 let action = { 294 data: { data: {}, type: "FOO" }, 295 target: msg.target, 296 }; 297 const expectedAction = { 298 type: action.data.type, 299 data: action.data.data, 300 _target: { browser: msg.data.browser }, 301 }; 302 mm.onMessage(action, msg.data); 303 assert.calledWith(mm.onActionFromContent, expectedAction, "foo"); 304 }); 305 }); 306 }); 307 describe("Sending and broadcasting", () => { 308 describe("#send", () => { 309 it("should send a message on the right port", () => { 310 let msg = getTabDetails("foo:6"); 311 mm.loadedTabs.set(msg.data.browser, msg.data); 312 const action = ac.AlsoToOneContent({ type: "HELLO" }, "foo:6"); 313 mm.send(action); 314 assert.calledWith( 315 msg.data.actor.sendAsyncMessage, 316 DEFAULT_OPTIONS.outgoingMessageName, 317 action 318 ); 319 }); 320 it("should not throw if the target isn't around", () => { 321 // port is not added to the channel 322 const action = ac.AlsoToOneContent({ type: "HELLO" }, "foo:7"); 323 324 assert.doesNotThrow(() => mm.send(action)); 325 }); 326 }); 327 describe("#broadcast", () => { 328 it("should send a message on the channel", () => { 329 let msg = getTabDetails("foo:8"); 330 mm.loadedTabs.set(msg.data.browser, msg.data); 331 const action = ac.BroadcastToContent({ type: "HELLO" }); 332 mm.broadcast(action); 333 assert.calledWith( 334 msg.data.actor.sendAsyncMessage, 335 DEFAULT_OPTIONS.outgoingMessageName, 336 action 337 ); 338 }); 339 }); 340 describe("#preloaded browser", () => { 341 it("should send the message to the preloaded browser if there's data and a preloaded browser exists", () => { 342 let msg = getTabDetails("foo:9", null, { preloaded: true }); 343 mm.loadedTabs.set(msg.data.browser, msg.data); 344 const action = ac.AlsoToPreloaded({ type: "HELLO", data: 10 }); 345 mm.sendToPreloaded(action); 346 assert.calledWith( 347 msg.data.actor.sendAsyncMessage, 348 DEFAULT_OPTIONS.outgoingMessageName, 349 action 350 ); 351 }); 352 it("should send the message to all the preloaded browsers if there's data and they exist", () => { 353 let msg1 = getTabDetails("foo:10a", null, { preloaded: true }); 354 mm.loadedTabs.set(msg1.data.browser, msg1.data); 355 356 let msg2 = getTabDetails("foo:10b", null, { preloaded: true }); 357 mm.loadedTabs.set(msg2.data.browser, msg2.data); 358 359 mm.sendToPreloaded(ac.AlsoToPreloaded({ type: "HELLO", data: 10 })); 360 assert.calledOnce(msg1.data.actor.sendAsyncMessage); 361 assert.calledOnce(msg2.data.actor.sendAsyncMessage); 362 }); 363 it("should not send the message to the preloaded browser if there's no data and a preloaded browser does not exists", () => { 364 let msg = getTabDetails("foo:11"); 365 mm.loadedTabs.set(msg.data.browser, msg.data); 366 const action = ac.AlsoToPreloaded({ type: "HELLO" }); 367 mm.sendToPreloaded(action); 368 assert.notCalled(msg.data.actor.sendAsyncMessage); 369 }); 370 }); 371 }); 372 describe("Handling actions", () => { 373 describe("#onActionFromContent", () => { 374 beforeEach(() => mm.onActionFromContent({ type: "FOO" }, "foo:12")); 375 it("should dispatch a AlsoToMain action", () => { 376 assert.calledOnce(dispatch); 377 const [action] = dispatch.firstCall.args; 378 assert.equal(action.type, "FOO", "action.type"); 379 }); 380 it("should have the right fromTarget", () => { 381 const [action] = dispatch.firstCall.args; 382 assert.equal(action.meta.fromTarget, "foo:12", "meta.fromTarget"); 383 }); 384 }); 385 describe("#middleware", () => { 386 let store; 387 beforeEach(() => { 388 store = createStore(addNumberReducer, applyMiddleware(mm.middleware)); 389 }); 390 it("should just call next if no channel is found", () => { 391 store.dispatch({ type: "ADD", data: 10 }); 392 assert.equal(store.getState(), 10); 393 }); 394 it("should call .send but not affect the main store if an OnlyToOneContent action is dispatched", () => { 395 sinon.stub(mm, "send"); 396 const action = ac.OnlyToOneContent({ type: "ADD", data: 10 }, "foo"); 397 398 store.dispatch(action); 399 400 assert.calledWith(mm.send, action); 401 assert.equal(store.getState(), 0); 402 }); 403 it("should call .send and update the main store if an AlsoToOneContent action is dispatched", () => { 404 sinon.stub(mm, "send"); 405 const action = ac.AlsoToOneContent({ type: "ADD", data: 10 }, "foo"); 406 407 store.dispatch(action); 408 409 assert.calledWith(mm.send, action); 410 assert.equal(store.getState(), 10); 411 }); 412 it("should call .broadcast if the action is BroadcastToContent", () => { 413 sinon.stub(mm, "broadcast"); 414 const action = ac.BroadcastToContent({ type: "FOO" }); 415 416 store.dispatch(action); 417 418 assert.calledWith(mm.broadcast, action); 419 }); 420 it("should call .sendToPreloaded if the action is AlsoToPreloaded", () => { 421 sinon.stub(mm, "sendToPreloaded"); 422 const action = ac.AlsoToPreloaded({ type: "FOO" }); 423 424 store.dispatch(action); 425 426 assert.calledWith(mm.sendToPreloaded, action); 427 }); 428 it("should dispatch other actions normally", () => { 429 sinon.stub(mm, "send"); 430 sinon.stub(mm, "broadcast"); 431 sinon.stub(mm, "sendToPreloaded"); 432 433 store.dispatch({ type: "ADD", data: 1 }); 434 435 assert.equal(store.getState(), 1); 436 assert.notCalled(mm.send); 437 assert.notCalled(mm.broadcast); 438 assert.notCalled(mm.sendToPreloaded); 439 }); 440 }); 441 }); 442 });