broadcastpubsub_test.js 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  1. // Copyright 2014 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. goog.provide('goog.labs.pubsub.BroadcastPubSubTest');
  15. goog.setTestOnly('goog.labs.pubsub.BroadcastPubSubTest');
  16. goog.require('goog.array');
  17. goog.require('goog.debug.Logger');
  18. goog.require('goog.json');
  19. goog.require('goog.labs.pubsub.BroadcastPubSub');
  20. goog.require('goog.storage.Storage');
  21. goog.require('goog.structs.Map');
  22. goog.require('goog.testing.MockClock');
  23. goog.require('goog.testing.MockControl');
  24. goog.require('goog.testing.events');
  25. goog.require('goog.testing.events.Event');
  26. goog.require('goog.testing.jsunit');
  27. goog.require('goog.testing.mockmatchers');
  28. goog.require('goog.testing.mockmatchers.ArgumentMatcher');
  29. goog.require('goog.testing.recordFunction');
  30. goog.require('goog.userAgent');
  31. /** @type {goog.labs.pubsub.BroadcastPubSub} */
  32. var broadcastPubSub;
  33. /** @type {goog.testing.MockControl} */
  34. var mockControl;
  35. /** @type {goog.testing.MockClock} */
  36. var mockClock;
  37. /** @type {goog.testing.MockInterface} */
  38. var mockStorage;
  39. /** @type {goog.testing.MockInterface} */
  40. var mockStorageCtor;
  41. /** @type {goog.structs.Map} */
  42. var mockHtml5LocalStorage;
  43. /** @type {goog.testing.MockInterface} */
  44. var mockHTML5LocalStorageCtor;
  45. /** @const {boolean} */
  46. var isIe8 = goog.userAgent.IE && goog.userAgent.DOCUMENT_MODE == 8;
  47. /**
  48. * Sends a remote storage event with special handling for IE8. With IE8 an
  49. * event is pushed to the event queue stored in local storage as a result of
  50. * behaviour by the mockHtml5LocalStorage instanciated when using IE8 an event
  51. * is automatically generated in the local browser context. For other browsers
  52. * this simply creates a new browser event.
  53. * @param {{'args': !Array<string>, 'timestamp': number}} data Value stored
  54. * in localStorage which generated the remote event.
  55. */
  56. function remoteStorageEvent(data) {
  57. if (!isIe8) {
  58. var event = new goog.testing.events.Event('storage', window);
  59. event.key = goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_;
  60. event.newValue = goog.json.serialize(data);
  61. goog.testing.events.fireBrowserEvent(event);
  62. } else {
  63. var uniqueKey =
  64. goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ + '1234567890';
  65. var ie8Events = mockHtml5LocalStorage.get(uniqueKey);
  66. if (goog.isDefAndNotNull(ie8Events)) {
  67. ie8Events = goog.json.parse(ie8Events);
  68. // Events should never overlap in IE8 mode.
  69. if (ie8Events.length > 0 &&
  70. ie8Events[ie8Events.length - 1]['timestamp'] >= data['timestamp']) {
  71. data['timestamp'] = ie8Events[ie8Events.length - 1]['timestamp'] +
  72. goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_;
  73. }
  74. } else {
  75. ie8Events = [];
  76. }
  77. ie8Events.push(data);
  78. // This will cause an event.
  79. mockHtml5LocalStorage.set(uniqueKey, goog.json.serialize(ie8Events));
  80. }
  81. }
  82. function setUp() {
  83. mockControl = new goog.testing.MockControl();
  84. mockClock = new goog.testing.MockClock(true);
  85. // Time should never be 0...
  86. mockClock.tick();
  87. /** @suppress {missingRequire} */
  88. mockHTML5LocalStorageCtor = mockControl.createConstructorMock(
  89. goog.storage.mechanism, 'HTML5LocalStorage');
  90. mockHtml5LocalStorage = new goog.structs.Map();
  91. // The builtin localStorage returns null instead of undefined.
  92. var originalGetFn =
  93. goog.bind(mockHtml5LocalStorage.get, mockHtml5LocalStorage);
  94. mockHtml5LocalStorage.get = function(key) {
  95. var value = originalGetFn(key);
  96. if (!goog.isDef(value)) {
  97. return null;
  98. }
  99. return value;
  100. };
  101. mockHtml5LocalStorage.key = function(idx) {
  102. return mockHtml5LocalStorage.getKeys()[idx];
  103. };
  104. mockHtml5LocalStorage.isAvailable = function() { return true; };
  105. // IE has problems. IE9+ still dispatches storage events locally. IE8 also
  106. // doesn't include the key/value information. So for IE, every time we get a
  107. // "set" on localStorage we simulate for the appropriate browser.
  108. if (goog.userAgent.IE) {
  109. var target = isIe8 ? document : window;
  110. var originalSetFn =
  111. goog.bind(mockHtml5LocalStorage.set, mockHtml5LocalStorage);
  112. mockHtml5LocalStorage.set = function(key, value) {
  113. originalSetFn(key, value);
  114. var event = new goog.testing.events.Event('storage', target);
  115. if (!isIe8) {
  116. event.key = key;
  117. event.newValue = value;
  118. }
  119. goog.testing.events.fireBrowserEvent(event);
  120. };
  121. }
  122. }
  123. function tearDown() {
  124. mockControl.$tearDown();
  125. mockClock.dispose();
  126. broadcastPubSub = undefined;
  127. }
  128. function testConstructor() {
  129. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  130. mockControl.$replayAll();
  131. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  132. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  133. assertNotNullNorUndefined(
  134. 'BroadcastChannel instance must not be null', broadcastPubSub);
  135. assertTrue(
  136. 'BroadcastChannel instance must have the expected type',
  137. broadcastPubSub instanceof goog.labs.pubsub.BroadcastPubSub);
  138. assertArrayEquals(
  139. goog.labs.pubsub.BroadcastPubSub.instances_, [broadcastPubSub]);
  140. broadcastPubSub.dispose();
  141. mockControl.$verifyAll();
  142. assertNotNullNorUndefined(
  143. 'Storage should not be undefined or null in broadcastPubSub.',
  144. broadcastPubSub.storage_);
  145. assertArrayEquals(goog.labs.pubsub.BroadcastPubSub.instances_, []);
  146. }
  147. function testConstructor_noLocalStorage() {
  148. mockHTML5LocalStorageCtor().$returns(
  149. {isAvailable: function() { return false; }});
  150. mockControl.$replayAll();
  151. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  152. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  153. assertNotNullNorUndefined(
  154. 'BroadcastChannel instance must not be null', broadcastPubSub);
  155. assertTrue(
  156. 'BroadcastChannel instance must have the expected type',
  157. broadcastPubSub instanceof goog.labs.pubsub.BroadcastPubSub);
  158. assertArrayEquals(
  159. goog.labs.pubsub.BroadcastPubSub.instances_, [broadcastPubSub]);
  160. broadcastPubSub.dispose();
  161. mockControl.$verifyAll();
  162. assertNull(
  163. 'Storage should be null in broadcastPubSub.', broadcastPubSub.storage_);
  164. assertArrayEquals(goog.labs.pubsub.BroadcastPubSub.instances_, []);
  165. }
  166. /** Verify we cleanup after ourselves. */
  167. function testDispose() {
  168. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  169. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  170. var mockStorage = mockControl.createLooseMock(goog.storage.Storage);
  171. var mockStorageCtor =
  172. mockControl.createConstructorMock(goog.storage, 'Storage');
  173. mockStorageCtor(mockHtml5LocalStorage).$returns(mockStorage);
  174. mockStorageCtor(mockHtml5LocalStorage).$returns(mockStorage);
  175. if (isIe8) {
  176. mockStorage.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
  177. }
  178. mockControl.$replayAll();
  179. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  180. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  181. var broadcastPubSubExtra = new goog.labs.pubsub.BroadcastPubSub();
  182. assertArrayEquals(
  183. goog.labs.pubsub.BroadcastPubSub.instances_,
  184. [broadcastPubSub, broadcastPubSubExtra]);
  185. assertFalse(
  186. 'BroadcastChannel extra instance must not have been disposed of',
  187. broadcastPubSubExtra.isDisposed());
  188. broadcastPubSubExtra.dispose();
  189. assertTrue(
  190. 'BroadcastChannel extra instance must have been disposed of',
  191. broadcastPubSubExtra.isDisposed());
  192. assertFalse(
  193. 'BroadcastChannel instance must not have been disposed of',
  194. broadcastPubSub.isDisposed());
  195. assertArrayEquals(
  196. goog.labs.pubsub.BroadcastPubSub.instances_, [broadcastPubSub]);
  197. assertFalse(
  198. 'BroadcastChannel instance must not have been disposed of',
  199. broadcastPubSub.isDisposed());
  200. broadcastPubSub.dispose();
  201. assertTrue(
  202. 'BroadcastChannel instance must have been disposed of',
  203. broadcastPubSub.isDisposed());
  204. assertArrayEquals(goog.labs.pubsub.BroadcastPubSub.instances_, []);
  205. mockControl.$verifyAll();
  206. }
  207. /**
  208. * Tests related to remote events that an instance of BroadcastChannel
  209. * should handle.
  210. */
  211. function testHandleRemoteEvent() {
  212. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  213. var foo = mockControl.createFunctionMock();
  214. foo('x', 'y').$times(2);
  215. var context = {'foo': 'bar'};
  216. var bar = goog.testing.recordFunction();
  217. mockControl.$replayAll();
  218. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  219. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  220. var eventData = {'args': ['someTopic', 'x', 'y'], 'timestamp': goog.now()};
  221. broadcastPubSub.subscribe('someTopic', foo);
  222. broadcastPubSub.subscribe('someTopic', bar, context);
  223. remoteStorageEvent(eventData);
  224. mockClock.tick();
  225. assertEquals(1, bar.getCallCount());
  226. assertEquals(context, bar.getLastCall().getThis());
  227. assertArrayEquals(['x', 'y'], bar.getLastCall().getArguments());
  228. broadcastPubSub.unsubscribe('someTopic', foo);
  229. eventData['timestamp'] = goog.now();
  230. remoteStorageEvent(eventData);
  231. mockClock.tick();
  232. assertEquals(2, bar.getCallCount());
  233. assertEquals(context, bar.getLastCall().getThis());
  234. assertArrayEquals(['x', 'y'], bar.getLastCall().getArguments());
  235. broadcastPubSub.subscribe('someTopic', foo);
  236. broadcastPubSub.unsubscribe('someTopic', bar, context);
  237. eventData['timestamp'] = goog.now();
  238. remoteStorageEvent(eventData);
  239. mockClock.tick();
  240. assertEquals(2, bar.getCallCount());
  241. broadcastPubSub.dispose();
  242. mockControl.$verifyAll();
  243. }
  244. function testHandleRemoteEventSubscribeOnce() {
  245. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  246. var foo = mockControl.createFunctionMock();
  247. foo('x', 'y');
  248. mockControl.$replayAll();
  249. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  250. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  251. broadcastPubSub.subscribeOnce('someTopic', foo);
  252. assertEquals(
  253. 'BroadcastChannel must have one subscriber', 1,
  254. broadcastPubSub.getCount());
  255. remoteStorageEvent(
  256. {'args': ['someTopic', 'x', 'y'], 'timestamp': goog.now()});
  257. mockClock.tick();
  258. assertEquals(
  259. 'BroadcastChannel must have no subscribers after receiving the event', 0,
  260. broadcastPubSub.getCount());
  261. broadcastPubSub.dispose();
  262. mockControl.$verifyAll();
  263. }
  264. function testHandleQueuedRemoteEvents() {
  265. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  266. var foo = mockControl.createFunctionMock();
  267. var bar = mockControl.createFunctionMock();
  268. foo('x', 'y');
  269. bar('d', 'c');
  270. mockControl.$replayAll();
  271. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  272. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  273. broadcastPubSub.subscribe('fooTopic', foo);
  274. broadcastPubSub.subscribe('barTopic', bar);
  275. var eventData = {'args': ['fooTopic', 'x', 'y'], 'timestamp': goog.now()};
  276. remoteStorageEvent(eventData);
  277. eventData = {'args': ['barTopic', 'd', 'c'], 'timestamp': goog.now()};
  278. remoteStorageEvent(eventData);
  279. mockClock.tick();
  280. broadcastPubSub.dispose();
  281. mockControl.$verifyAll();
  282. }
  283. function testHandleRemoteEventsUnsubscribe() {
  284. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  285. var foo = mockControl.createFunctionMock();
  286. var bar = mockControl.createFunctionMock();
  287. foo('x', 'y').$does(function() {
  288. broadcastPubSub.unsubscribe('barTopic', bar);
  289. });
  290. mockControl.$replayAll();
  291. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  292. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  293. broadcastPubSub.subscribe('fooTopic', foo);
  294. broadcastPubSub.subscribe('barTopic', bar);
  295. var eventData = {'args': ['fooTopic', 'x', 'y'], 'timestamp': goog.now()};
  296. remoteStorageEvent(eventData);
  297. mockClock.tick();
  298. eventData = {'args': ['barTopic', 'd', 'c'], 'timestamp': goog.now()};
  299. remoteStorageEvent(eventData);
  300. mockClock.tick();
  301. broadcastPubSub.dispose();
  302. mockControl.$verifyAll();
  303. }
  304. function testHandleRemoteEventsCalledOnce() {
  305. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  306. var foo = mockControl.createFunctionMock();
  307. foo('x', 'y');
  308. mockControl.$replayAll();
  309. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  310. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  311. broadcastPubSub.subscribeOnce('someTopic', foo);
  312. var eventData = {'args': ['someTopic', 'x', 'y'], 'timestamp': goog.now()};
  313. remoteStorageEvent(eventData);
  314. mockClock.tick();
  315. eventData = {'args': ['someTopic', 'x', 'y'], 'timestamp': goog.now()};
  316. remoteStorageEvent(eventData);
  317. mockClock.tick();
  318. broadcastPubSub.dispose();
  319. mockControl.$verifyAll();
  320. }
  321. function testHandleRemoteEventNestedPublish() {
  322. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  323. var foo1 = mockControl.createFunctionMock();
  324. foo1().$does(function() {
  325. remoteStorageEvent({'args': ['bar'], 'timestamp': goog.now()});
  326. });
  327. var foo2 = mockControl.createFunctionMock();
  328. foo2();
  329. var bar1 = mockControl.createFunctionMock();
  330. bar1().$does(function() { broadcastPubSub.publish('baz'); });
  331. var bar2 = mockControl.createFunctionMock();
  332. bar2();
  333. var baz1 = mockControl.createFunctionMock();
  334. baz1();
  335. var baz2 = mockControl.createFunctionMock();
  336. baz2();
  337. mockControl.$replayAll();
  338. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  339. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  340. broadcastPubSub.subscribe('foo', foo1);
  341. broadcastPubSub.subscribe('foo', foo2);
  342. broadcastPubSub.subscribe('bar', bar1);
  343. broadcastPubSub.subscribe('bar', bar2);
  344. broadcastPubSub.subscribe('baz', baz1);
  345. broadcastPubSub.subscribe('baz', baz2);
  346. remoteStorageEvent({'args': ['foo'], 'timestamp': goog.now()});
  347. mockClock.tick();
  348. broadcastPubSub.dispose();
  349. mockControl.$verifyAll();
  350. }
  351. /**
  352. * Local publish that originated from another instance of BroadcastChannel
  353. * in the same Javascript context.
  354. */
  355. function testSecondInstancePublish() {
  356. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage).$times(2);
  357. var foo = mockControl.createFunctionMock();
  358. foo('x', 'y');
  359. var context = {'foo': 'bar'};
  360. var bar = goog.testing.recordFunction();
  361. mockControl.$replayAll();
  362. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  363. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  364. broadcastPubSub.subscribe('someTopic', foo);
  365. broadcastPubSub.subscribe('someTopic', bar, context);
  366. var broadcastPubSub2 = new goog.labs.pubsub.BroadcastPubSub();
  367. broadcastPubSub2.publish('someTopic', 'x', 'y');
  368. mockClock.tick();
  369. assertEquals(1, bar.getCallCount());
  370. assertEquals(context, bar.getLastCall().getThis());
  371. assertArrayEquals(['x', 'y'], bar.getLastCall().getArguments());
  372. broadcastPubSub.dispose();
  373. mockControl.$verifyAll();
  374. }
  375. function testSecondInstanceNestedPublish() {
  376. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage).$times(2);
  377. var foo = mockControl.createFunctionMock();
  378. foo('m', 'n').$does(function() {
  379. broadcastPubSub.publish('barTopic', 'd', 'c');
  380. });
  381. var bar = mockControl.createFunctionMock();
  382. bar('d', 'c');
  383. mockControl.$replayAll();
  384. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  385. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  386. broadcastPubSub.subscribe('fooTopic', foo);
  387. var broadcastPubSub2 = new goog.labs.pubsub.BroadcastPubSub();
  388. broadcastPubSub2.subscribe('barTopic', bar);
  389. broadcastPubSub2.publish('fooTopic', 'm', 'n');
  390. mockClock.tick();
  391. mockClock.tick();
  392. broadcastPubSub.dispose();
  393. mockControl.$verifyAll();
  394. }
  395. /**
  396. * Validate the localStorage data is being set as we expect.
  397. */
  398. function testLocalStorageData() {
  399. var topic = 'someTopic';
  400. var anotherTopic = 'anotherTopic';
  401. var now = goog.now();
  402. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  403. var mockStorage = mockControl.createLooseMock(goog.storage.Storage);
  404. var mockStorageCtor =
  405. mockControl.createConstructorMock(goog.storage, 'Storage');
  406. mockStorageCtor(mockHtml5LocalStorage).$returns(mockStorage);
  407. if (!isIe8) {
  408. mockStorage.set(
  409. goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_,
  410. {'args': [topic, '10'], 'timestamp': now});
  411. mockStorage.remove(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_);
  412. mockStorage.set(
  413. goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_,
  414. {'args': [anotherTopic, '13'], 'timestamp': now});
  415. mockStorage.remove(goog.labs.pubsub.BroadcastPubSub.STORAGE_KEY_);
  416. } else {
  417. var firstEventArray = [{'args': [topic, '10'], 'timestamp': now}];
  418. var secondEventArray = [
  419. {'args': [topic, '10'], 'timestamp': now}, {
  420. 'args': [anotherTopic, '13'],
  421. 'timestamp': now +
  422. goog.labs.pubsub.BroadcastPubSub.IE8_TIMESTAMP_UNIQUE_OFFSET_MS_
  423. }
  424. ];
  425. mockStorage.get(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_)
  426. .$returns(null);
  427. mockStorage.set(
  428. goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_,
  429. new goog.testing.mockmatchers.ArgumentMatcher(function(val) {
  430. return goog.testing.mockmatchers.flexibleArrayMatcher(
  431. firstEventArray, val);
  432. }, 'First event array'));
  433. // Make sure to clone or you're going to have a bad time.
  434. mockStorage.get(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_)
  435. .$returns(goog.array.clone(firstEventArray));
  436. mockStorage.set(
  437. goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_,
  438. new goog.testing.mockmatchers.ArgumentMatcher(function(val) {
  439. return goog.testing.mockmatchers.flexibleArrayMatcher(
  440. secondEventArray, val);
  441. }, 'Second event array'));
  442. mockStorage.remove(goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_);
  443. }
  444. var fn = goog.testing.recordFunction();
  445. fn('10');
  446. mockControl.$replayAll();
  447. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  448. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  449. broadcastPubSub.subscribe(topic, fn);
  450. broadcastPubSub.publish(topic, '10');
  451. broadcastPubSub.publish(anotherTopic, '13');
  452. mockClock.tick();
  453. broadcastPubSub.dispose();
  454. mockControl.$verifyAll();
  455. }
  456. function testBrokenTimestamp() {
  457. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  458. var fn = mockControl.createFunctionMock();
  459. mockControl.$replayAll();
  460. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  461. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  462. broadcastPubSub.subscribe('someTopic', fn);
  463. remoteStorageEvent({'args': 'WAT?', 'timestamp': 'wat?'});
  464. mockClock.tick();
  465. broadcastPubSub.dispose();
  466. mockControl.$verifyAll();
  467. }
  468. /** Test response to bad localStorage data. */
  469. function testBrokenEvent() {
  470. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  471. var fn = mockControl.createFunctionMock();
  472. mockControl.$replayAll();
  473. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  474. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  475. broadcastPubSub.subscribe('someTopic', fn);
  476. if (!isIe8) {
  477. var event = new goog.testing.events.Event('storage', window);
  478. event.key = 'FooBarBaz';
  479. event.newValue = goog.json.serialize({'keyby': 'word'});
  480. goog.testing.events.fireBrowserEvent(event);
  481. } else {
  482. var uniqueKey =
  483. goog.labs.pubsub.BroadcastPubSub.IE8_EVENTS_KEY_PREFIX_ + '1234567890';
  484. // This will cause an event.
  485. mockHtml5LocalStorage.set(uniqueKey, 'Toothpaste!');
  486. }
  487. mockClock.tick();
  488. broadcastPubSub.dispose();
  489. mockControl.$verifyAll();
  490. }
  491. /**
  492. * The following tests are duplicated from pubsub because they depend
  493. * on functionality (mostly "publish") that has changed in BroadcastChannel.
  494. */
  495. function testPublish() {
  496. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  497. var foo = mockControl.createFunctionMock();
  498. foo('x', 'y');
  499. var context = {'foo': 'bar'};
  500. var bar = goog.testing.recordFunction();
  501. var baz = mockControl.createFunctionMock();
  502. baz('d', 'c');
  503. mockControl.$replayAll();
  504. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  505. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  506. broadcastPubSub.subscribe('someTopic', foo);
  507. broadcastPubSub.subscribe('someTopic', bar, context);
  508. broadcastPubSub.subscribe('anotherTopic', baz, context);
  509. broadcastPubSub.publish('someTopic', 'x', 'y');
  510. mockClock.tick();
  511. assertTrue(broadcastPubSub.unsubscribe('someTopic', foo));
  512. broadcastPubSub.publish('anotherTopic', 'd', 'c');
  513. broadcastPubSub.publish('someTopic', 'x', 'y');
  514. mockClock.tick();
  515. broadcastPubSub.subscribe('differentTopic', foo);
  516. broadcastPubSub.publish('someTopic', 'x', 'y');
  517. mockClock.tick();
  518. assertEquals(3, bar.getCallCount());
  519. goog.array.forEach(bar.getCalls(), function(call) {
  520. assertArrayEquals(['x', 'y'], call.getArguments());
  521. assertEquals(context, call.getThis());
  522. });
  523. broadcastPubSub.dispose();
  524. mockControl.$verifyAll();
  525. }
  526. function testPublishEmptyTopic() {
  527. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  528. var foo = mockControl.createFunctionMock();
  529. foo();
  530. mockControl.$replayAll();
  531. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  532. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  533. broadcastPubSub.publish('someTopic');
  534. mockClock.tick();
  535. broadcastPubSub.subscribe('someTopic', foo);
  536. broadcastPubSub.publish('someTopic');
  537. mockClock.tick();
  538. broadcastPubSub.unsubscribe('someTopic', foo);
  539. broadcastPubSub.publish('someTopic');
  540. mockClock.tick();
  541. broadcastPubSub.dispose();
  542. mockControl.$verifyAll();
  543. }
  544. function testSubscribeWhilePublishing() {
  545. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  546. // It's OK for a subscriber to add a new subscriber to its own topic,
  547. // but the newly added subscriber shouldn't be called until the next
  548. // publish cycle.
  549. var fn1 = mockControl.createFunctionMock();
  550. var fn2 = mockControl.createFunctionMock();
  551. fn1()
  552. .$does(function() { broadcastPubSub.subscribe('someTopic', fn2); })
  553. .$times(2);
  554. fn2();
  555. mockControl.$replayAll();
  556. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  557. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  558. broadcastPubSub.subscribe('someTopic', fn1);
  559. assertEquals(
  560. 'Topic must have one subscriber', 1,
  561. broadcastPubSub.getCount('someTopic'));
  562. broadcastPubSub.publish('someTopic');
  563. mockClock.tick();
  564. assertEquals(
  565. 'Topic must have two subscribers', 2,
  566. broadcastPubSub.getCount('someTopic'));
  567. broadcastPubSub.publish('someTopic');
  568. mockClock.tick();
  569. assertEquals(
  570. 'Topic must have three subscribers', 3,
  571. broadcastPubSub.getCount('someTopic'));
  572. broadcastPubSub.dispose();
  573. mockControl.$verifyAll();
  574. }
  575. function testUnsubscribeWhilePublishing() {
  576. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  577. // It's OK for a subscriber to unsubscribe another subscriber from its
  578. // own topic, but the subscriber in question won't actually be removed
  579. // until after publishing is complete.
  580. var fn1 = mockControl.createFunctionMock();
  581. var fn2 = mockControl.createFunctionMock();
  582. var fn3 = mockControl.createFunctionMock();
  583. fn1().$does(function() {
  584. assertTrue(
  585. 'unsubscribe() must return true when removing a topic',
  586. broadcastPubSub.unsubscribe('X', fn2));
  587. assertEquals(
  588. 'Topic "X" must still have 3 subscribers', 3,
  589. broadcastPubSub.getCount('X'));
  590. });
  591. fn2().$times(0);
  592. fn3().$does(function() {
  593. assertTrue(
  594. 'unsubscribe() must return true when removing a topic',
  595. broadcastPubSub.unsubscribe('X', fn1));
  596. assertEquals(
  597. 'Topic "X" must still have 3 subscribers', 3,
  598. broadcastPubSub.getCount('X'));
  599. });
  600. mockControl.$replayAll();
  601. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  602. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  603. broadcastPubSub.subscribe('X', fn1);
  604. broadcastPubSub.subscribe('X', fn2);
  605. broadcastPubSub.subscribe('X', fn3);
  606. assertEquals(
  607. 'Topic "X" must have 3 subscribers', 3, broadcastPubSub.getCount('X'));
  608. broadcastPubSub.publish('X');
  609. mockClock.tick();
  610. assertEquals(
  611. 'Topic "X" must have 1 subscriber after publishing', 1,
  612. broadcastPubSub.getCount('X'));
  613. assertEquals(
  614. 'BroadcastChannel must not have any subscriptions pending removal', 0,
  615. broadcastPubSub.pubSub_.pendingKeys_.length);
  616. broadcastPubSub.dispose();
  617. mockControl.$verifyAll();
  618. }
  619. function testUnsubscribeSelfWhilePublishing() {
  620. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  621. // It's OK for a subscriber to unsubscribe itself, but it won't actually
  622. // be removed until after publishing is complete.
  623. var fn = mockControl.createFunctionMock();
  624. fn().$does(function() {
  625. assertTrue(
  626. 'unsubscribe() must return true when removing a topic',
  627. broadcastPubSub.unsubscribe('someTopic', fn));
  628. assertEquals(
  629. 'Topic must still have 1 subscriber', 1,
  630. broadcastPubSub.getCount('someTopic'));
  631. });
  632. mockControl.$replayAll();
  633. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  634. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  635. broadcastPubSub.subscribe('someTopic', fn);
  636. assertEquals(
  637. 'Topic must have 1 subscriber', 1, broadcastPubSub.getCount('someTopic'));
  638. broadcastPubSub.publish('someTopic');
  639. mockClock.tick();
  640. assertEquals(
  641. 'Topic must have no subscribers after publishing', 0,
  642. broadcastPubSub.getCount('someTopic'));
  643. assertEquals(
  644. 'BroadcastChannel must not have any subscriptions pending removal', 0,
  645. broadcastPubSub.pubSub_.pendingKeys_.length);
  646. broadcastPubSub.dispose();
  647. mockControl.$verifyAll();
  648. }
  649. function testNestedPublish() {
  650. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  651. var xFn1 = mockControl.createFunctionMock();
  652. xFn1().$does(function() {
  653. broadcastPubSub.publish('Y');
  654. broadcastPubSub.unsubscribe('X', arguments.callee);
  655. });
  656. var xFn2 = mockControl.createFunctionMock();
  657. xFn2();
  658. var yFn1 = mockControl.createFunctionMock();
  659. yFn1().$does(function() {
  660. broadcastPubSub.unsubscribe('Y', arguments.callee);
  661. });
  662. var yFn2 = mockControl.createFunctionMock();
  663. yFn2();
  664. mockControl.$replayAll();
  665. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  666. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  667. broadcastPubSub.subscribe('X', xFn1);
  668. broadcastPubSub.subscribe('X', xFn2);
  669. broadcastPubSub.subscribe('Y', yFn1);
  670. broadcastPubSub.subscribe('Y', yFn2);
  671. broadcastPubSub.publish('X');
  672. mockClock.tick();
  673. broadcastPubSub.dispose();
  674. mockControl.$verifyAll();
  675. }
  676. function testSubscribeOnce() {
  677. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  678. var fn = goog.testing.recordFunction();
  679. mockControl.$replayAll();
  680. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  681. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  682. broadcastPubSub.subscribeOnce('someTopic', fn);
  683. assertEquals(
  684. 'Topic must have one subscriber', 1,
  685. broadcastPubSub.getCount('someTopic'));
  686. broadcastPubSub.publish('someTopic');
  687. mockClock.tick();
  688. assertEquals(
  689. 'Topic must have no subscribers', 0,
  690. broadcastPubSub.getCount('someTopic'));
  691. var context = {'foo': 'bar'};
  692. broadcastPubSub.subscribeOnce('someTopic', fn, context);
  693. assertEquals(
  694. 'Topic must have one subscriber', 1,
  695. broadcastPubSub.getCount('someTopic'));
  696. assertEquals(
  697. 'Subscriber must not have been called yet', 1, fn.getCallCount());
  698. broadcastPubSub.publish('someTopic');
  699. mockClock.tick();
  700. assertEquals(
  701. 'Topic must have no subscribers', 0,
  702. broadcastPubSub.getCount('someTopic'));
  703. assertEquals('Subscriber must have been called', 2, fn.getCallCount());
  704. assertEquals(context, fn.getLastCall().getThis());
  705. assertArrayEquals([], fn.getLastCall().getArguments());
  706. context = {'foo': 'bar'};
  707. broadcastPubSub.subscribeOnce('someTopic', fn, context);
  708. assertEquals(
  709. 'Topic must have one subscriber', 1,
  710. broadcastPubSub.getCount('someTopic'));
  711. assertEquals('Subscriber must not have been called', 2, fn.getCallCount());
  712. broadcastPubSub.publish('someTopic', '17');
  713. mockClock.tick();
  714. assertEquals(
  715. 'Topic must have no subscribers', 0,
  716. broadcastPubSub.getCount('someTopic'));
  717. assertEquals(context, fn.getLastCall().getThis());
  718. assertEquals('Subscriber must have been called', 3, fn.getCallCount());
  719. assertArrayEquals(['17'], fn.getLastCall().getArguments());
  720. broadcastPubSub.dispose();
  721. mockControl.$verifyAll();
  722. }
  723. function testSubscribeOnce_boundFn() {
  724. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  725. var fn = goog.testing.recordFunction();
  726. var context = {'foo': 'bar'};
  727. mockControl.$replayAll();
  728. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  729. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  730. broadcastPubSub.subscribeOnce('someTopic', goog.bind(fn, context));
  731. assertEquals(
  732. 'Topic must have one subscriber', 1,
  733. broadcastPubSub.getCount('someTopic'));
  734. assertNull('Subscriber must not have been called yet', fn.getLastCall());
  735. broadcastPubSub.publish('someTopic', '17');
  736. mockClock.tick();
  737. assertEquals(
  738. 'Topic must have no subscribers', 0,
  739. broadcastPubSub.getCount('someTopic'));
  740. assertEquals('Subscriber must have been called', 1, fn.getCallCount());
  741. assertEquals(
  742. 'Must receive correct argument.', '17', fn.getLastCall().getArgument(0));
  743. assertEquals(
  744. 'Must have appropriate context.', context, fn.getLastCall().getThis());
  745. broadcastPubSub.dispose();
  746. mockControl.$verifyAll();
  747. }
  748. function testSubscribeOnce_partialFn() {
  749. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  750. var fullFn = mockControl.createFunctionMock();
  751. fullFn(true, '17');
  752. mockControl.$replayAll();
  753. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  754. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  755. broadcastPubSub.subscribeOnce('someTopic', goog.partial(fullFn, true));
  756. assertEquals(
  757. 'Topic must have one subscriber', 1,
  758. broadcastPubSub.getCount('someTopic'));
  759. broadcastPubSub.publish('someTopic', '17');
  760. mockClock.tick();
  761. assertEquals(
  762. 'Topic must have no subscribers', 0,
  763. broadcastPubSub.getCount('someTopic'));
  764. broadcastPubSub.dispose();
  765. mockControl.$verifyAll();
  766. }
  767. function testSelfResubscribe() {
  768. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  769. var resubscribeFn = mockControl.createFunctionMock();
  770. var resubscribe = function() {
  771. broadcastPubSub.subscribeOnce('someTopic', resubscribeFn);
  772. };
  773. resubscribeFn('foo').$does(resubscribe);
  774. resubscribeFn('bar').$does(resubscribe);
  775. resubscribeFn('baz').$does(resubscribe);
  776. mockControl.$replayAll();
  777. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  778. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  779. broadcastPubSub.subscribeOnce('someTopic', resubscribeFn);
  780. assertEquals(
  781. 'Topic must have 1 subscriber', 1, broadcastPubSub.getCount('someTopic'));
  782. broadcastPubSub.publish('someTopic', 'foo');
  783. mockClock.tick();
  784. assertEquals(
  785. 'Topic must have 1 subscriber', 1, broadcastPubSub.getCount('someTopic'));
  786. assertEquals(
  787. 'BroadcastChannel must not have any pending unsubscribe keys', 0,
  788. broadcastPubSub.pubSub_.pendingKeys_.length);
  789. broadcastPubSub.publish('someTopic', 'bar');
  790. mockClock.tick();
  791. assertEquals(
  792. 'Topic must have 1 subscriber', 1, broadcastPubSub.getCount('someTopic'));
  793. assertEquals(
  794. 'BroadcastChannel must not have any pending unsubscribe keys', 0,
  795. broadcastPubSub.pubSub_.pendingKeys_.length);
  796. broadcastPubSub.publish('someTopic', 'baz');
  797. mockClock.tick();
  798. assertEquals(
  799. 'Topic must have 1 subscriber', 1, broadcastPubSub.getCount('someTopic'));
  800. assertEquals(
  801. 'BroadcastChannel must not have any pending unsubscribe keys', 0,
  802. broadcastPubSub.pubSub_.pendingKeys_.length);
  803. broadcastPubSub.dispose();
  804. mockControl.$verifyAll();
  805. }
  806. function testClear() {
  807. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  808. var fn = mockControl.createFunctionMock();
  809. mockControl.$replayAll();
  810. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  811. broadcastPubSub.logger_.setLevel(goog.debug.Logger.Level.OFF);
  812. goog.array.forEach(['V', 'W', 'X', 'Y', 'Z'], function(topic) {
  813. broadcastPubSub.subscribe(topic, fn);
  814. });
  815. assertEquals(
  816. 'BroadcastChannel must have 5 subscribers', 5,
  817. broadcastPubSub.getCount());
  818. broadcastPubSub.clear('W');
  819. assertEquals(
  820. 'BroadcastChannel must have 4 subscribers', 4,
  821. broadcastPubSub.getCount());
  822. goog.array.forEach(
  823. ['X', 'Y'], function(topic) { broadcastPubSub.clear(topic); });
  824. assertEquals(
  825. 'BroadcastChannel must have 2 subscriber', 2, broadcastPubSub.getCount());
  826. broadcastPubSub.clear();
  827. assertEquals(
  828. 'BroadcastChannel must have no subscribers', 0,
  829. broadcastPubSub.getCount());
  830. broadcastPubSub.dispose();
  831. mockControl.$verifyAll();
  832. }
  833. function testNestedSubscribeOnce() {
  834. mockHTML5LocalStorageCtor().$returns(mockHtml5LocalStorage);
  835. var x = mockControl.createFunctionMock();
  836. var y = mockControl.createFunctionMock();
  837. x().$times(1);
  838. y().$does(function() {
  839. broadcastPubSub.publish('X');
  840. broadcastPubSub.publish('X');
  841. });
  842. mockControl.$replayAll();
  843. broadcastPubSub = new goog.labs.pubsub.BroadcastPubSub();
  844. broadcastPubSub.subscribeOnce('X', x);
  845. broadcastPubSub.subscribe('Y', y);
  846. broadcastPubSub.publish('Y');
  847. mockClock.tick();
  848. broadcastPubSub.dispose();
  849. mockControl.$verifyAll();
  850. }