SectionsManager.test.js (27944B)
1 "use strict"; 2 import { 3 actionCreators as ac, 4 actionTypes as at, 5 CONTENT_MESSAGE_TYPE, 6 MAIN_MESSAGE_TYPE, 7 PRELOAD_MESSAGE_TYPE, 8 } from "common/Actions.mjs"; 9 import { EventEmitter, GlobalOverrider } from "test/unit/utils"; 10 import { SectionsFeed, SectionsManager } from "lib/SectionsManager.sys.mjs"; 11 12 const FAKE_ID = "FAKE_ID"; 13 const FAKE_OPTIONS = { icon: "FAKE_ICON", title: "FAKE_TITLE" }; 14 const FAKE_ROWS = [ 15 { url: "1.example.com", type: "bookmark" }, 16 { url: "2.example.com", type: "pocket" }, 17 { url: "3.example.com", type: "history" }, 18 ]; 19 const FAKE_TRENDING_ROWS = [{ url: "bar", type: "trending" }]; 20 const FAKE_URL = "2.example.com"; 21 const FAKE_CARD_OPTIONS = { title: "Some fake title" }; 22 23 describe("SectionsManager", () => { 24 let globals; 25 let fakeServices; 26 let fakePlacesUtils; 27 let sandbox; 28 29 beforeEach(async () => { 30 sandbox = sinon.createSandbox(); 31 globals = new GlobalOverrider(); 32 fakeServices = { 33 prefs: { 34 getBoolPref: sandbox.stub(), 35 addObserver: sandbox.stub(), 36 removeObserver: sandbox.stub(), 37 }, 38 }; 39 fakePlacesUtils = { 40 history: { update: sinon.stub(), insert: sinon.stub() }, 41 }; 42 globals.set({ 43 Services: fakeServices, 44 PlacesUtils: fakePlacesUtils, 45 NimbusFeatures: { 46 newtab: { getAllVariables: sandbox.stub() }, 47 pocketNewtab: { getAllVariables: sandbox.stub() }, 48 }, 49 }); 50 // Redecorate SectionsManager to remove any listeners that have been added 51 EventEmitter.decorate(SectionsManager); 52 }); 53 54 afterEach(() => { 55 globals.restore(); 56 sandbox.restore(); 57 }); 58 59 describe("#init", () => { 60 it("should initialise the sections map with the built in sections", async () => { 61 SectionsManager.sections.clear(); 62 SectionsManager.initialized = false; 63 await SectionsManager.init({}); 64 assert.equal(SectionsManager.sections.size, 2); 65 assert.ok(SectionsManager.sections.has("topstories")); 66 assert.ok(SectionsManager.sections.has("highlights")); 67 }); 68 it("should set .initialized to true", async () => { 69 SectionsManager.sections.clear(); 70 SectionsManager.initialized = false; 71 await SectionsManager.init({}); 72 assert.ok(SectionsManager.initialized); 73 }); 74 it("should add observer for context menu prefs", async () => { 75 SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: "MENU_ITEM_PREF" }; 76 await SectionsManager.init({}); 77 assert.calledOnce(fakeServices.prefs.addObserver); 78 assert.calledWith( 79 fakeServices.prefs.addObserver, 80 "MENU_ITEM_PREF", 81 SectionsManager 82 ); 83 }); 84 }); 85 describe("#uninit", () => { 86 it("should remove observer for context menu prefs", () => { 87 SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: "MENU_ITEM_PREF" }; 88 SectionsManager.initialized = true; 89 SectionsManager.uninit(); 90 assert.calledOnce(fakeServices.prefs.removeObserver); 91 assert.calledWith( 92 fakeServices.prefs.removeObserver, 93 "MENU_ITEM_PREF", 94 SectionsManager 95 ); 96 assert.isFalse(SectionsManager.initialized); 97 }); 98 }); 99 describe("#addBuiltInSection", () => { 100 it("should not report an error if options is undefined", async () => { 101 globals.sandbox.spy(global.console, "error"); 102 await SectionsManager.addBuiltInSection( 103 "feeds.section.topstories", 104 undefined 105 ); 106 107 assert.notCalled(console.error); 108 }); 109 it("should report an error if options is malformed", async () => { 110 globals.sandbox.spy(global.console, "error"); 111 await SectionsManager.addBuiltInSection( 112 "feeds.section.topstories", 113 "invalid" 114 ); 115 116 assert.calledOnce(console.error); 117 }); 118 }); 119 describe("#addSection", () => { 120 it("should add the id to sections and emit an ADD_SECTION event", () => { 121 const spy = sinon.spy(); 122 SectionsManager.on(SectionsManager.ADD_SECTION, spy); 123 SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); 124 assert.ok(SectionsManager.sections.has(FAKE_ID)); 125 assert.calledOnce(spy); 126 assert.calledWith( 127 spy, 128 SectionsManager.ADD_SECTION, 129 FAKE_ID, 130 FAKE_OPTIONS 131 ); 132 }); 133 }); 134 describe("#removeSection", () => { 135 it("should remove the id from sections and emit an REMOVE_SECTION event", () => { 136 // Ensure we start with the id in the set 137 assert.ok(SectionsManager.sections.has(FAKE_ID)); 138 const spy = sinon.spy(); 139 SectionsManager.on(SectionsManager.REMOVE_SECTION, spy); 140 SectionsManager.removeSection(FAKE_ID); 141 assert.notOk(SectionsManager.sections.has(FAKE_ID)); 142 assert.calledOnce(spy); 143 assert.calledWith(spy, SectionsManager.REMOVE_SECTION, FAKE_ID); 144 }); 145 }); 146 describe("#enableSection", () => { 147 it("should call updateSection with {enabled: true}", () => { 148 sinon.spy(SectionsManager, "updateSection"); 149 SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); 150 SectionsManager.enableSection(FAKE_ID); 151 assert.calledOnce(SectionsManager.updateSection); 152 assert.calledWith( 153 SectionsManager.updateSection, 154 FAKE_ID, 155 { enabled: true }, 156 true 157 ); 158 SectionsManager.updateSection.restore(); 159 }); 160 it("should emit an ENABLE_SECTION event", () => { 161 const spy = sinon.spy(); 162 SectionsManager.on(SectionsManager.ENABLE_SECTION, spy); 163 SectionsManager.enableSection(FAKE_ID); 164 assert.calledOnce(spy); 165 assert.calledWith(spy, SectionsManager.ENABLE_SECTION, FAKE_ID); 166 }); 167 }); 168 describe("#disableSection", () => { 169 it("should call updateSection with {enabled: false, rows: [], initialized: false}", () => { 170 sinon.spy(SectionsManager, "updateSection"); 171 SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); 172 SectionsManager.disableSection(FAKE_ID); 173 assert.calledOnce(SectionsManager.updateSection); 174 assert.calledWith( 175 SectionsManager.updateSection, 176 FAKE_ID, 177 { enabled: false, rows: [], initialized: false }, 178 true 179 ); 180 SectionsManager.updateSection.restore(); 181 }); 182 it("should emit a DISABLE_SECTION event", () => { 183 const spy = sinon.spy(); 184 SectionsManager.on(SectionsManager.DISABLE_SECTION, spy); 185 SectionsManager.disableSection(FAKE_ID); 186 assert.calledOnce(spy); 187 assert.calledWith(spy, SectionsManager.DISABLE_SECTION, FAKE_ID); 188 }); 189 }); 190 describe("#updateSection", () => { 191 it("should emit an UPDATE_SECTION event with correct arguments", () => { 192 SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); 193 const spy = sinon.spy(); 194 const dedupeConfigurations = [ 195 { id: "topstories", dedupeFrom: ["highlights"] }, 196 ]; 197 SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); 198 SectionsManager.updateSection(FAKE_ID, { rows: FAKE_ROWS }, true); 199 assert.calledOnce(spy); 200 assert.calledWith( 201 spy, 202 SectionsManager.UPDATE_SECTION, 203 FAKE_ID, 204 { rows: FAKE_ROWS, dedupeConfigurations }, 205 true 206 ); 207 }); 208 it("should do nothing if the section doesn't exist", () => { 209 SectionsManager.removeSection(FAKE_ID); 210 const spy = sinon.spy(); 211 SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); 212 SectionsManager.updateSection(FAKE_ID, { rows: FAKE_ROWS }, true); 213 assert.notCalled(spy); 214 }); 215 it("should update all sections", () => { 216 SectionsManager.sections.clear(); 217 const updateSectionOrig = SectionsManager.updateSection; 218 SectionsManager.updateSection = sinon.spy(); 219 220 SectionsManager.addSection("ID1", { title: "FAKE_TITLE_1" }); 221 SectionsManager.addSection("ID2", { title: "FAKE_TITLE_2" }); 222 SectionsManager.updateSections(); 223 224 assert.calledTwice(SectionsManager.updateSection); 225 assert.calledWith( 226 SectionsManager.updateSection, 227 "ID1", 228 { title: "FAKE_TITLE_1" }, 229 true 230 ); 231 assert.calledWith( 232 SectionsManager.updateSection, 233 "ID2", 234 { title: "FAKE_TITLE_2" }, 235 true 236 ); 237 SectionsManager.updateSection = updateSectionOrig; 238 }); 239 it("context menu pref change should update sections", async () => { 240 let observer; 241 const services = { 242 prefs: { 243 getBoolPref: sinon.spy(), 244 addObserver: (pref, o) => (observer = o), 245 removeObserver: sinon.spy(), 246 }, 247 }; 248 globals.set("Services", services); 249 250 SectionsManager.updateSections = sinon.spy(); 251 SectionsManager.CONTEXT_MENU_PREFS = { MENU_ITEM: "MENU_ITEM_PREF" }; 252 await SectionsManager.init({}); 253 observer.observe("", "nsPref:changed", "MENU_ITEM_PREF"); 254 255 assert.calledOnce(SectionsManager.updateSections); 256 }); 257 }); 258 describe("#_addCardTypeLinkMenuOptions", () => { 259 const addCardTypeLinkMenuOptionsOrig = 260 SectionsManager._addCardTypeLinkMenuOptions; 261 const contextMenuOptionsOrig = 262 SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES; 263 beforeEach(() => { 264 // Add a topstories section and a highlights section, with types for each card 265 SectionsManager.addSection("topstories", { FAKE_TRENDING_ROWS }); 266 SectionsManager.addSection("highlights", { FAKE_ROWS }); 267 }); 268 it("should only call _addCardTypeLinkMenuOptions if the section update is for highlights", () => { 269 SectionsManager._addCardTypeLinkMenuOptions = sinon.spy(); 270 SectionsManager.updateSection("topstories", { rows: FAKE_ROWS }, false); 271 assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions); 272 273 SectionsManager.updateSection("highlights", { rows: FAKE_ROWS }, false); 274 assert.calledWith(SectionsManager._addCardTypeLinkMenuOptions, FAKE_ROWS); 275 }); 276 it("should only call _addCardTypeLinkMenuOptions if the section update has rows", () => { 277 SectionsManager._addCardTypeLinkMenuOptions = sinon.spy(); 278 SectionsManager.updateSection("highlights", {}, false); 279 assert.notCalled(SectionsManager._addCardTypeLinkMenuOptions); 280 }); 281 it("should assign the correct context menu options based on the type of highlight", () => { 282 SectionsManager._addCardTypeLinkMenuOptions = 283 addCardTypeLinkMenuOptionsOrig; 284 285 SectionsManager.updateSection("highlights", { rows: FAKE_ROWS }, false); 286 const highlights = SectionsManager.sections.get("highlights").FAKE_ROWS; 287 288 // FAKE_ROWS was added in the following order: bookmark, pocket, history 289 assert.deepEqual( 290 highlights[0].contextMenuOptions, 291 SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.bookmark 292 ); 293 assert.deepEqual( 294 highlights[1].contextMenuOptions, 295 SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.pocket 296 ); 297 assert.deepEqual( 298 highlights[2].contextMenuOptions, 299 SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES.history 300 ); 301 }); 302 it("should throw an error if you are assigning a context menu to a non-existant highlight type", () => { 303 globals.sandbox.spy(global.console, "error"); 304 SectionsManager.updateSection( 305 "highlights", 306 { rows: [{ url: "foo", type: "badtype" }] }, 307 false 308 ); 309 const highlights = SectionsManager.sections.get("highlights").rows; 310 assert.calledOnce(console.error); 311 assert.equal(highlights[0].contextMenuOptions, undefined); 312 }); 313 it("should filter out context menu options that are in CONTEXT_MENU_PREFS", () => { 314 const services = { 315 prefs: { 316 getBoolPref: o => 317 SectionsManager.CONTEXT_MENU_PREFS[o] !== "RemoveMe", 318 addObserver() {}, 319 removeObserver() {}, 320 }, 321 }; 322 globals.set("Services", services); 323 SectionsManager.CONTEXT_MENU_PREFS = { RemoveMe: "RemoveMe" }; 324 SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = { 325 bookmark: ["KeepMe", "RemoveMe"], 326 pocket: ["KeepMe", "RemoveMe"], 327 history: ["KeepMe", "RemoveMe"], 328 }; 329 SectionsManager.updateSection("highlights", { rows: FAKE_ROWS }, false); 330 const highlights = SectionsManager.sections.get("highlights").FAKE_ROWS; 331 332 // Only keep context menu options that were not supposed to be removed based on CONTEXT_MENU_PREFS 333 assert.deepEqual(highlights[0].contextMenuOptions, ["KeepMe"]); 334 assert.deepEqual(highlights[1].contextMenuOptions, ["KeepMe"]); 335 assert.deepEqual(highlights[2].contextMenuOptions, ["KeepMe"]); 336 SectionsManager.CONTEXT_MENU_OPTIONS_FOR_HIGHLIGHT_TYPES = 337 contextMenuOptionsOrig; 338 globals.restore(); 339 }); 340 }); 341 describe("#onceInitialized", () => { 342 it("should call the callback immediately if SectionsManager is initialised", () => { 343 SectionsManager.initialized = true; 344 const callback = sinon.spy(); 345 SectionsManager.onceInitialized(callback); 346 assert.calledOnce(callback); 347 }); 348 it("should bind the callback to .once(INIT) if SectionsManager is not initialised", () => { 349 SectionsManager.initialized = false; 350 sinon.spy(SectionsManager, "once"); 351 const callback = () => {}; 352 SectionsManager.onceInitialized(callback); 353 assert.calledOnce(SectionsManager.once); 354 assert.calledWith(SectionsManager.once, SectionsManager.INIT, callback); 355 }); 356 }); 357 describe("#updateSectionCard", () => { 358 it("should emit an UPDATE_SECTION_CARD event with correct arguments", () => { 359 SectionsManager.addSection( 360 FAKE_ID, 361 Object.assign({}, FAKE_OPTIONS, { rows: FAKE_ROWS }) 362 ); 363 const spy = sinon.spy(); 364 SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy); 365 SectionsManager.updateSectionCard( 366 FAKE_ID, 367 FAKE_URL, 368 FAKE_CARD_OPTIONS, 369 true 370 ); 371 assert.calledOnce(spy); 372 assert.calledWith( 373 spy, 374 SectionsManager.UPDATE_SECTION_CARD, 375 FAKE_ID, 376 FAKE_URL, 377 FAKE_CARD_OPTIONS, 378 true 379 ); 380 }); 381 it("should do nothing if the section doesn't exist", () => { 382 SectionsManager.removeSection(FAKE_ID); 383 const spy = sinon.spy(); 384 SectionsManager.on(SectionsManager.UPDATE_SECTION_CARD, spy); 385 SectionsManager.updateSectionCard( 386 FAKE_ID, 387 FAKE_URL, 388 FAKE_CARD_OPTIONS, 389 true 390 ); 391 assert.notCalled(spy); 392 }); 393 }); 394 describe("#removeSectionCard", () => { 395 it("should dispatch an SECTION_UPDATE action in which cards corresponding to the given url are removed", () => { 396 const rows = [{ url: "foo.com" }, { url: "bar.com" }]; 397 398 SectionsManager.addSection( 399 FAKE_ID, 400 Object.assign({}, FAKE_OPTIONS, { rows }) 401 ); 402 const spy = sinon.spy(); 403 SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); 404 SectionsManager.removeSectionCard(FAKE_ID, "foo.com"); 405 406 assert.calledOnce(spy); 407 assert.equal(spy.firstCall.args[1], FAKE_ID); 408 assert.deepEqual(spy.firstCall.args[2].rows, [{ url: "bar.com" }]); 409 }); 410 it("should do nothing if the section doesn't exist", () => { 411 SectionsManager.removeSection(FAKE_ID); 412 const spy = sinon.spy(); 413 SectionsManager.on(SectionsManager.UPDATE_SECTION, spy); 414 SectionsManager.removeSectionCard(FAKE_ID, "bar.com"); 415 assert.notCalled(spy); 416 }); 417 }); 418 describe("#updateBookmarkMetadata", () => { 419 beforeEach(() => { 420 let rows = [ 421 { 422 url: "bar", 423 title: "title", 424 description: "description", 425 image: "image", 426 type: "trending", 427 }, 428 ]; 429 SectionsManager.addSection("topstories", { rows }); 430 // Simulate 2 sections. 431 rows = [ 432 { 433 url: "foo", 434 title: "title", 435 description: "description", 436 image: "image", 437 type: "bookmark", 438 }, 439 ]; 440 SectionsManager.addSection("highlights", { rows }); 441 }); 442 443 it("shouldn't call PlacesUtils if URL is not in topstories", () => { 444 SectionsManager.updateBookmarkMetadata({ url: "foo" }); 445 446 assert.notCalled(fakePlacesUtils.history.update); 447 }); 448 it("should call PlacesUtils.history.update", () => { 449 SectionsManager.updateBookmarkMetadata({ url: "bar" }); 450 451 assert.calledOnce(fakePlacesUtils.history.update); 452 assert.calledWithExactly(fakePlacesUtils.history.update, { 453 url: "bar", 454 title: "title", 455 description: "description", 456 previewImageURL: "image", 457 }); 458 }); 459 it("should call PlacesUtils.history.insert", () => { 460 SectionsManager.updateBookmarkMetadata({ url: "bar" }); 461 462 assert.calledOnce(fakePlacesUtils.history.insert); 463 assert.calledWithExactly(fakePlacesUtils.history.insert, { 464 url: "bar", 465 title: "title", 466 visits: [{}], 467 }); 468 }); 469 }); 470 }); 471 472 describe("SectionsFeed", () => { 473 let feed; 474 let sandbox; 475 let globals; 476 477 beforeEach(() => { 478 sandbox = sinon.createSandbox(); 479 SectionsManager.sections.clear(); 480 SectionsManager.initialized = false; 481 globals = new GlobalOverrider(); 482 globals.set("NimbusFeatures", { 483 newtab: { getAllVariables: sandbox.stub() }, 484 pocketNewtab: { getAllVariables: sandbox.stub() }, 485 }); 486 feed = new SectionsFeed(); 487 feed.store = { dispatch: sinon.spy() }; 488 feed.store = { 489 dispatch: sinon.spy(), 490 getState() { 491 return this.state; 492 }, 493 state: { 494 Prefs: { 495 values: { 496 sectionOrder: "topsites,topstories,highlights", 497 "feeds.topsites": true, 498 }, 499 }, 500 Sections: [{ initialized: false }], 501 }, 502 }; 503 }); 504 afterEach(() => { 505 feed.uninit(); 506 globals.restore(); 507 }); 508 describe("#init", () => { 509 it("should create a SectionsFeed", () => { 510 assert.instanceOf(feed, SectionsFeed); 511 }); 512 it("should bind appropriate listeners", () => { 513 sinon.spy(SectionsManager, "on"); 514 feed.init(); 515 assert.callCount(SectionsManager.on, 4); 516 for (const [event, listener] of [ 517 [SectionsManager.ADD_SECTION, feed.onAddSection], 518 [SectionsManager.REMOVE_SECTION, feed.onRemoveSection], 519 [SectionsManager.UPDATE_SECTION, feed.onUpdateSection], 520 [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard], 521 ]) { 522 assert.calledWith(SectionsManager.on, event, listener); 523 } 524 }); 525 it("should call onAddSection for any already added sections in SectionsManager", async () => { 526 await SectionsManager.init({}); 527 assert.ok(SectionsManager.sections.has("topstories")); 528 assert.ok(SectionsManager.sections.has("highlights")); 529 const topstories = SectionsManager.sections.get("topstories"); 530 const highlights = SectionsManager.sections.get("highlights"); 531 sinon.spy(feed, "onAddSection"); 532 feed.init(); 533 assert.calledTwice(feed.onAddSection); 534 assert.calledWith( 535 feed.onAddSection, 536 SectionsManager.ADD_SECTION, 537 "topstories", 538 topstories 539 ); 540 assert.calledWith( 541 feed.onAddSection, 542 SectionsManager.ADD_SECTION, 543 "highlights", 544 highlights 545 ); 546 }); 547 }); 548 describe("#uninit", () => { 549 it("should unbind all listeners", () => { 550 sinon.spy(SectionsManager, "off"); 551 feed.init(); 552 feed.uninit(); 553 assert.callCount(SectionsManager.off, 4); 554 for (const [event, listener] of [ 555 [SectionsManager.ADD_SECTION, feed.onAddSection], 556 [SectionsManager.REMOVE_SECTION, feed.onRemoveSection], 557 [SectionsManager.UPDATE_SECTION, feed.onUpdateSection], 558 [SectionsManager.UPDATE_SECTION_CARD, feed.onUpdateSectionCard], 559 ]) { 560 assert.calledWith(SectionsManager.off, event, listener); 561 } 562 }); 563 it("should emit an UNINIT event and set SectionsManager.initialized to false", () => { 564 const spy = sinon.spy(); 565 SectionsManager.on(SectionsManager.UNINIT, spy); 566 feed.init(); 567 feed.uninit(); 568 assert.calledOnce(spy); 569 assert.notOk(SectionsManager.initialized); 570 }); 571 }); 572 describe("#onAddSection", () => { 573 it("should broadcast a SECTION_REGISTER action with the correct data", () => { 574 feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS); 575 const [action] = feed.store.dispatch.firstCall.args; 576 assert.equal(action.type, "SECTION_REGISTER"); 577 assert.deepEqual( 578 action.data, 579 Object.assign({ id: FAKE_ID }, FAKE_OPTIONS) 580 ); 581 assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); 582 assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); 583 }); 584 it("should prepend id to sectionOrder pref if not already included", () => { 585 feed.store.state.Sections = [ 586 { id: "topstories", enabled: true }, 587 { id: "highlights", enabled: true }, 588 ]; 589 feed.onAddSection(null, FAKE_ID, FAKE_OPTIONS); 590 assert.calledWith(feed.store.dispatch, { 591 data: { 592 name: "sectionOrder", 593 value: `${FAKE_ID},topsites,topstories,highlights`, 594 }, 595 meta: { from: "ActivityStream:Content", to: "ActivityStream:Main" }, 596 type: "SET_PREF", 597 }); 598 }); 599 }); 600 describe("#onRemoveSection", () => { 601 it("should broadcast a SECTION_DEREGISTER action with the correct data", () => { 602 feed.onRemoveSection(null, FAKE_ID); 603 const [action] = feed.store.dispatch.firstCall.args; 604 assert.equal(action.type, "SECTION_DEREGISTER"); 605 assert.deepEqual(action.data, FAKE_ID); 606 // Should be broadcast 607 assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); 608 assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); 609 }); 610 }); 611 describe("#onUpdateSection", () => { 612 it("should do nothing if no options are provided", () => { 613 feed.onUpdateSection(null, FAKE_ID, null); 614 assert.notCalled(feed.store.dispatch); 615 }); 616 it("should dispatch a SECTION_UPDATE action with the correct data", () => { 617 feed.onUpdateSection(null, FAKE_ID, { rows: FAKE_ROWS }); 618 const [action] = feed.store.dispatch.firstCall.args; 619 assert.equal(action.type, "SECTION_UPDATE"); 620 assert.deepEqual(action.data, { id: FAKE_ID, rows: FAKE_ROWS }); 621 // Should be not broadcast by default, but should update the preloaded tab, so check meta 622 assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); 623 assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE); 624 }); 625 it("should broadcast the action only if shouldBroadcast is true", () => { 626 feed.onUpdateSection(null, FAKE_ID, { rows: FAKE_ROWS }, true); 627 const [action] = feed.store.dispatch.firstCall.args; 628 // Should be broadcast 629 assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); 630 assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); 631 }); 632 }); 633 describe("#onUpdateSectionCard", () => { 634 it("should do nothing if no options are provided", () => { 635 feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, null); 636 assert.notCalled(feed.store.dispatch); 637 }); 638 it("should dispatch a SECTION_UPDATE_CARD action with the correct data", () => { 639 feed.onUpdateSectionCard(null, FAKE_ID, FAKE_URL, FAKE_CARD_OPTIONS); 640 const [action] = feed.store.dispatch.firstCall.args; 641 assert.equal(action.type, "SECTION_UPDATE_CARD"); 642 assert.deepEqual(action.data, { 643 id: FAKE_ID, 644 url: FAKE_URL, 645 options: FAKE_CARD_OPTIONS, 646 }); 647 // Should be not broadcast by default, but should update the preloaded tab, so check meta 648 assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); 649 assert.equal(action.meta.to, PRELOAD_MESSAGE_TYPE); 650 }); 651 it("should broadcast the action only if shouldBroadcast is true", () => { 652 feed.onUpdateSectionCard( 653 null, 654 FAKE_ID, 655 FAKE_URL, 656 FAKE_CARD_OPTIONS, 657 true 658 ); 659 const [action] = feed.store.dispatch.firstCall.args; 660 // Should be broadcast 661 assert.equal(action.meta.from, MAIN_MESSAGE_TYPE); 662 assert.equal(action.meta.to, CONTENT_MESSAGE_TYPE); 663 }); 664 }); 665 describe("#onAction", () => { 666 it("should bind this.init to SectionsManager.INIT on INIT", () => { 667 sinon.spy(SectionsManager, "once"); 668 feed.onAction({ type: "INIT" }); 669 assert.calledOnce(SectionsManager.once); 670 assert.calledWith(SectionsManager.once, SectionsManager.INIT, feed.init); 671 }); 672 it("should call SectionsManager.addBuiltInSection on suitable PREF_CHANGED events", () => { 673 sinon.spy(SectionsManager, "addBuiltInSection"); 674 feed.onAction({ 675 type: "PREF_CHANGED", 676 data: { name: "feeds.section.topstories.options", value: "foo" }, 677 }); 678 assert.calledOnce(SectionsManager.addBuiltInSection); 679 assert.calledWith( 680 SectionsManager.addBuiltInSection, 681 "feeds.section.topstories", 682 "foo" 683 ); 684 }); 685 it("should fire SECTION_OPTIONS_UPDATED on suitable PREF_CHANGED events", async () => { 686 await feed.onAction({ 687 type: "PREF_CHANGED", 688 data: { name: "feeds.section.topstories.options", value: "foo" }, 689 }); 690 assert.calledOnce(feed.store.dispatch); 691 const [action] = feed.store.dispatch.firstCall.args; 692 assert.equal(action.type, "SECTION_OPTIONS_CHANGED"); 693 assert.equal(action.data, "topstories"); 694 }); 695 it("should call SectionsManager.disableSection on SECTION_DISABLE", () => { 696 sinon.spy(SectionsManager, "disableSection"); 697 feed.onAction({ type: "SECTION_DISABLE", data: 1234 }); 698 assert.calledOnce(SectionsManager.disableSection); 699 assert.calledWith(SectionsManager.disableSection, 1234); 700 SectionsManager.disableSection.restore(); 701 }); 702 it("should call SectionsManager.enableSection on SECTION_ENABLE", () => { 703 sinon.spy(SectionsManager, "enableSection"); 704 feed.onAction({ type: "SECTION_ENABLE", data: 1234 }); 705 assert.calledOnce(SectionsManager.enableSection); 706 assert.calledWith(SectionsManager.enableSection, 1234); 707 SectionsManager.enableSection.restore(); 708 }); 709 it("should call the feed's uninit on UNINIT", () => { 710 sinon.stub(feed, "uninit"); 711 712 feed.onAction({ type: "UNINIT" }); 713 714 assert.calledOnce(feed.uninit); 715 }); 716 it("should emit a ACTION_DISPATCHED event and forward any action in ACTIONS_TO_PROXY if there are any sections", () => { 717 const spy = sinon.spy(); 718 const allowedActions = SectionsManager.ACTIONS_TO_PROXY; 719 const disallowedActions = ["PREF_CHANGED", "OPEN_PRIVATE_WINDOW"]; 720 feed.init(); 721 SectionsManager.on(SectionsManager.ACTION_DISPATCHED, spy); 722 // Make sure we start with no sections - no event should be emitted 723 SectionsManager.sections.clear(); 724 feed.onAction({ type: allowedActions[0] }); 725 assert.notCalled(spy); 726 // Then add a section and check correct behaviour 727 SectionsManager.addSection(FAKE_ID, FAKE_OPTIONS); 728 for (const action of allowedActions.concat(disallowedActions)) { 729 feed.onAction({ type: action }); 730 } 731 for (const action of allowedActions) { 732 assert.calledWith(spy, "ACTION_DISPATCHED", action); 733 } 734 for (const action of disallowedActions) { 735 assert.neverCalledWith(spy, "ACTION_DISPATCHED", action); 736 } 737 }); 738 it("should call updateBookmarkMetadata on PLACES_BOOKMARK_ADDED", () => { 739 const stub = sinon.stub(SectionsManager, "updateBookmarkMetadata"); 740 741 feed.onAction({ type: "PLACES_BOOKMARK_ADDED", data: {} }); 742 743 assert.calledOnce(stub); 744 }); 745 it("should call SectionManager.removeSectionCard on WEBEXT_DISMISS", () => { 746 const stub = sinon.stub(SectionsManager, "removeSectionCard"); 747 748 feed.onAction( 749 ac.WebExtEvent(at.WEBEXT_DISMISS, { source: "Foo", url: "bar.com" }) 750 ); 751 752 assert.calledOnce(stub); 753 assert.calledWith(stub, "Foo", "bar.com"); 754 }); 755 }); 756 });