ActivityStreamMessageChannel.test.js 15 KB

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