tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });