tor-browser

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

test_HighlightsFeed.js (38081B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 ChromeUtils.defineESModuleGetters(this, {
      7  actionTypes: "resource://newtab/common/Actions.mjs",
      8  BOOKMARKS_RESTORE_SUCCESS_EVENT:
      9    "resource://newtab/lib/HighlightsFeed.sys.mjs",
     10  BOOKMARKS_RESTORE_FAILED_EVENT:
     11    "resource://newtab/lib/HighlightsFeed.sys.mjs",
     12  FilterAdult: "resource:///modules/FilterAdult.sys.mjs",
     13  HighlightsFeed: "resource://newtab/lib/HighlightsFeed.sys.mjs",
     14  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
     15  PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
     16  sinon: "resource://testing-common/Sinon.sys.mjs",
     17  Screenshots: "resource://newtab/lib/Screenshots.sys.mjs",
     18  SectionsManager: "resource://newtab/lib/SectionsManager.sys.mjs",
     19  SECTION_ID: "resource://newtab/lib/HighlightsFeed.sys.mjs",
     20  SYNC_BOOKMARKS_FINISHED_EVENT: "resource://newtab/lib/HighlightsFeed.sys.mjs",
     21 });
     22 
     23 const FAKE_LINKS = new Array(20)
     24  .fill(null)
     25  .map((v, i) => ({ url: `http://www.site${i}.com` }));
     26 const FAKE_IMAGE = "data123";
     27 const FAKE_URL = "https://mozilla.org";
     28 const FAKE_IMAGE_URL = "https://mozilla.org/preview.jpg";
     29 
     30 function getHighlightsFeedForTest(sandbox) {
     31  let feed = new HighlightsFeed();
     32  feed.store = {
     33    dispatch: sandbox.spy(),
     34    getState() {
     35      return this.state;
     36    },
     37    state: {
     38      Prefs: {
     39        values: {
     40          "section.highlights.includeDownloads": false,
     41        },
     42      },
     43      TopSites: {
     44        initialized: true,
     45        rows: Array(12)
     46          .fill(null)
     47          .map((v, i) => ({ url: `http://www.topsite${i}.com` })),
     48      },
     49      Sections: [{ id: "highlights", initialized: false }],
     50    },
     51    subscribe: sandbox.stub().callsFake(cb => {
     52      cb();
     53      return () => {};
     54    }),
     55  };
     56 
     57  sandbox
     58    .stub(NewTabUtils.activityStreamLinks, "getHighlights")
     59    .resolves(FAKE_LINKS);
     60  sandbox
     61    .stub(NewTabUtils.activityStreamProvider, "_processHighlights")
     62    .callsFake(l => l.slice(0, 1));
     63 
     64  return feed;
     65 }
     66 
     67 async function fetchHighlightsRows(feed, options) {
     68  let sandbox = sinon.createSandbox();
     69  sandbox.stub(SectionsManager, "updateSection");
     70  await feed.fetchHighlights(options);
     71  let [, { rows }] = SectionsManager.updateSection.firstCall.args;
     72 
     73  sandbox.restore();
     74  return rows;
     75 }
     76 
     77 function fetchImage(feed, page) {
     78  return feed.fetchImage(
     79    Object.assign({ __sharedCache: { updateLink() {} } }, page)
     80  );
     81 }
     82 
     83 add_task(function test_construction() {
     84  info("HighlightsFeed construction should work");
     85  let sandbox = sinon.createSandbox();
     86  sandbox.stub(PageThumbs, "addExpirationFilter");
     87 
     88  let feed = getHighlightsFeedForTest(sandbox);
     89  Assert.ok(feed, "Was able to create a HighlightsFeed");
     90 
     91  info("HighlightsFeed construction should add a PageThumbs expiration filter");
     92  Assert.ok(
     93    PageThumbs.addExpirationFilter.calledOnce,
     94    "PageThumbs.addExpirationFilter was called once"
     95  );
     96 
     97  sandbox.restore();
     98 });
     99 
    100 add_task(function test_init_action() {
    101  let sandbox = sinon.createSandbox();
    102 
    103  let countObservers = topic => {
    104    return [...Services.obs.enumerateObservers(topic)].length;
    105  };
    106 
    107  const INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT = countObservers(
    108    SYNC_BOOKMARKS_FINISHED_EVENT
    109  );
    110  const INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT = countObservers(
    111    BOOKMARKS_RESTORE_SUCCESS_EVENT
    112  );
    113  const INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT = countObservers(
    114    BOOKMARKS_RESTORE_FAILED_EVENT
    115  );
    116 
    117  sandbox
    118    .stub(SectionsManager, "onceInitialized")
    119    .callsFake(callback => callback());
    120  sandbox.stub(SectionsManager, "enableSection");
    121 
    122  let feed = getHighlightsFeedForTest(sandbox);
    123  sandbox.stub(feed, "fetchHighlights");
    124  sandbox.stub(feed.downloadsManager, "init");
    125 
    126  feed.onAction({ type: actionTypes.INIT });
    127 
    128  info("HighlightsFeed.onAction(INIT) should add a sync observer");
    129  Assert.equal(
    130    countObservers(SYNC_BOOKMARKS_FINISHED_EVENT),
    131    INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT + 1
    132  );
    133  Assert.equal(
    134    countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT),
    135    INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT + 1
    136  );
    137  Assert.equal(
    138    countObservers(BOOKMARKS_RESTORE_FAILED_EVENT),
    139    INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT + 1
    140  );
    141 
    142  info(
    143    "HighlightsFeed.onAction(INIT) should call SectionsManager.onceInitialized"
    144  );
    145  Assert.ok(
    146    SectionsManager.onceInitialized.calledOnce,
    147    "SectionsManager.onceInitialized was called"
    148  );
    149 
    150  info("HighlightsFeed.onAction(INIT) should enable its section");
    151  Assert.ok(
    152    SectionsManager.enableSection.calledOnce,
    153    "SectionsManager.enableSection was called"
    154  );
    155  Assert.ok(SectionsManager.enableSection.calledWith(SECTION_ID));
    156 
    157  info("HighlightsFeed.onAction(INIT) should fetch highlights");
    158  Assert.ok(
    159    feed.fetchHighlights.calledOnce,
    160    "HighlightsFeed.fetchHighlights was called"
    161  );
    162 
    163  info("HighlightsFeed.onAction(INIT) should initialize the DownloadsManager");
    164  Assert.ok(
    165    feed.downloadsManager.init.calledOnce,
    166    "HighlightsFeed.downloadsManager.init was called"
    167  );
    168 
    169  feed.uninit();
    170  // Let's make sure that uninit also removed these observers while we're here.
    171  Assert.equal(
    172    countObservers(SYNC_BOOKMARKS_FINISHED_EVENT),
    173    INITIAL_SYNC_BOOKMARKS_FINISHED_EVENT_COUNT
    174  );
    175  Assert.equal(
    176    countObservers(BOOKMARKS_RESTORE_SUCCESS_EVENT),
    177    INITIAL_BOOKMARKS_RESTORE_SUCCESS_EVENT_COUNT
    178  );
    179  Assert.equal(
    180    countObservers(BOOKMARKS_RESTORE_FAILED_EVENT),
    181    INITIAL_BOOKMARKS_RESTORE_FAILED_EVENT_COUNT
    182  );
    183 
    184  sandbox.restore();
    185 });
    186 
    187 add_task(async function test_observe_fetch_highlights() {
    188  let topicDataPairs = [
    189    {
    190      description:
    191        "should fetch highlights when we are done a sync for bookmarks",
    192      shouldFetch: true,
    193      topic: SYNC_BOOKMARKS_FINISHED_EVENT,
    194      data: "bookmarks",
    195    },
    196    {
    197      description: "should fetch highlights after a successful import",
    198      shouldFetch: true,
    199      topic: BOOKMARKS_RESTORE_SUCCESS_EVENT,
    200      data: "html",
    201    },
    202    {
    203      description: "should fetch highlights after a failed import",
    204      shouldFetch: true,
    205      topic: BOOKMARKS_RESTORE_FAILED_EVENT,
    206      data: "json",
    207    },
    208    {
    209      description:
    210        "should not fetch highlights when we are doing a sync for something that is not bookmarks",
    211      shouldFetch: false,
    212      topic: SYNC_BOOKMARKS_FINISHED_EVENT,
    213      data: "tabs",
    214    },
    215    {
    216      description: "should not fetch highlights after a successful import",
    217      shouldFetch: false,
    218      topic: "someotherevent",
    219      data: "bookmarks",
    220    },
    221  ];
    222 
    223  for (let topicDataPair of topicDataPairs) {
    224    info(`HighlightsFeed.observe ${topicDataPair.description}`);
    225    let sandbox = sinon.createSandbox();
    226    let feed = getHighlightsFeedForTest(sandbox);
    227    sandbox.stub(feed, "fetchHighlights");
    228    feed.observe(null, topicDataPair.topic, topicDataPair.data);
    229 
    230    if (topicDataPair.shouldFetch) {
    231      Assert.ok(
    232        feed.fetchHighlights.calledOnce,
    233        "HighlightsFeed.fetchHighlights was called"
    234      );
    235      Assert.ok(feed.fetchHighlights.calledWith({ broadcast: true }));
    236    } else {
    237      Assert.ok(
    238        feed.fetchHighlights.notCalled,
    239        "HighlightsFeed.fetchHighlights was not called"
    240      );
    241    }
    242 
    243    sandbox.restore();
    244  }
    245 });
    246 
    247 add_task(async function test_filterForThumbnailExpiration_calls() {
    248  info(
    249    "HighlightsFeed.filterForThumbnailExpiration should pass rows.urls " +
    250      "to the callback provided"
    251  );
    252  let sandbox = sinon.createSandbox();
    253  let feed = getHighlightsFeedForTest(sandbox);
    254  let rows = [{ url: "foo.com" }, { url: "bar.com" }];
    255 
    256  feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }];
    257  const stub = sinon.stub();
    258 
    259  feed.filterForThumbnailExpiration(stub);
    260 
    261  Assert.ok(stub.calledOnce, "Filter was called");
    262  Assert.ok(stub.calledWithExactly(rows.map(r => r.url)));
    263 
    264  sandbox.restore();
    265 });
    266 
    267 add_task(
    268  async function test_filterForThumbnailExpiration_include_preview_image_url() {
    269    info(
    270      "HighlightsFeed.filterForThumbnailExpiration should include " +
    271        "preview_image_url (if present) in the callback results"
    272    );
    273    let sandbox = sinon.createSandbox();
    274    let feed = getHighlightsFeedForTest(sandbox);
    275    let rows = [
    276      { url: "foo.com" },
    277      { url: "bar.com", preview_image_url: "bar.jpg" },
    278    ];
    279 
    280    feed.store.state.Sections = [{ id: "highlights", rows, initialized: true }];
    281    const stub = sinon.stub();
    282 
    283    feed.filterForThumbnailExpiration(stub);
    284 
    285    Assert.ok(stub.calledOnce, "Filter was called");
    286    Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "bar.jpg"]));
    287 
    288    sandbox.restore();
    289  }
    290 );
    291 
    292 add_task(async function test_filterForThumbnailExpiration_not_initialized() {
    293  info(
    294    "HighlightsFeed.filterForThumbnailExpiration should pass an empty " +
    295      "array if not initialized"
    296  );
    297  let sandbox = sinon.createSandbox();
    298  let feed = getHighlightsFeedForTest(sandbox);
    299  let rows = [{ url: "foo.com" }, { url: "bar.com" }];
    300 
    301  feed.store.state.Sections = [{ rows, initialized: false }];
    302  const stub = sinon.stub();
    303 
    304  feed.filterForThumbnailExpiration(stub);
    305 
    306  Assert.ok(stub.calledOnce, "Filter was called");
    307  Assert.ok(stub.calledWithExactly([]));
    308 
    309  sandbox.restore();
    310 });
    311 
    312 add_task(async function test_fetchHighlights_TopSites_not_initialized() {
    313  info(
    314    "HighlightsFeed.fetchHighlights should return early if TopSites are not " +
    315      "initialized"
    316  );
    317  let sandbox = sinon.createSandbox();
    318  let feed = getHighlightsFeedForTest(sandbox);
    319 
    320  sandbox.spy(feed.linksCache, "request");
    321 
    322  feed.store.state.TopSites.initialized = false;
    323  feed.store.state.Prefs.values["feeds.topsites"] = true;
    324  feed.store.state.Prefs.values["feeds.system.topsites"] = true;
    325 
    326  // Initially TopSites is uninitialised and fetchHighlights should return.
    327  await feed.fetchHighlights();
    328 
    329  Assert.ok(
    330    NewTabUtils.activityStreamLinks.getHighlights.notCalled,
    331    "NewTabUtils.activityStreamLinks.getHighlights was not called"
    332  );
    333  Assert.ok(
    334    feed.linksCache.request.notCalled,
    335    "HighlightsFeed.linksCache.request was not called"
    336  );
    337 
    338  sandbox.restore();
    339 });
    340 
    341 add_task(async function test_fetchHighlights_sections_not_initialized() {
    342  info(
    343    "HighlightsFeed.fetchHighlights should return early if Sections are not " +
    344      "initialized"
    345  );
    346  let sandbox = sinon.createSandbox();
    347  let feed = getHighlightsFeedForTest(sandbox);
    348 
    349  sandbox.spy(feed.linksCache, "request");
    350 
    351  feed.store.state.TopSites.initialized = true;
    352  feed.store.state.Prefs.values["feeds.topsites"] = true;
    353  feed.store.state.Prefs.values["feeds.system.topsites"] = true;
    354  feed.store.state.Sections = [];
    355 
    356  await feed.fetchHighlights();
    357 
    358  Assert.ok(
    359    NewTabUtils.activityStreamLinks.getHighlights.notCalled,
    360    "NewTabUtils.activityStreamLinks.getHighlights was not called"
    361  );
    362  Assert.ok(
    363    feed.linksCache.request.notCalled,
    364    "HighlightsFeed.linksCache.request was not called"
    365  );
    366 
    367  sandbox.restore();
    368 });
    369 
    370 add_task(async function test_fetchHighlights_TopSites_initialized() {
    371  info(
    372    "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites are " +
    373      "initialised"
    374  );
    375  let sandbox = sinon.createSandbox();
    376  let feed = getHighlightsFeedForTest(sandbox);
    377 
    378  sandbox.spy(feed.linksCache, "request");
    379 
    380  // fetchHighlights should continue
    381  feed.store.state.TopSites.initialized = true;
    382 
    383  await feed.fetchHighlights();
    384 
    385  Assert.ok(
    386    NewTabUtils.activityStreamLinks.getHighlights.calledOnce,
    387    "NewTabUtils.activityStreamLinks.getHighlights was called"
    388  );
    389  Assert.ok(
    390    feed.linksCache.request.calledOnce,
    391    "HighlightsFeed.linksCache.request was called"
    392  );
    393 
    394  sandbox.restore();
    395 });
    396 
    397 add_task(async function test_fetchHighlights_chronological_order() {
    398  info(
    399    "HighlightsFeed.fetchHighlights should chronologically order highlight " +
    400      "data types"
    401  );
    402  let sandbox = sinon.createSandbox();
    403  let feed = getHighlightsFeedForTest(sandbox);
    404 
    405  let links = [
    406    {
    407      url: "https://site0.com",
    408      type: "bookmark",
    409      bookmarkGuid: "1234",
    410      date_added: Date.now() - 80,
    411    }, // 3rd newest
    412    {
    413      url: "https://site1.com",
    414      type: "history",
    415      bookmarkGuid: "1234",
    416      date_added: Date.now() - 60,
    417    }, // append at the end
    418    {
    419      url: "https://site2.com",
    420      type: "history",
    421      date_added: Date.now() - 160,
    422    }, // append at the end
    423    {
    424      url: "https://site3.com",
    425      type: "history",
    426      date_added: Date.now() - 60,
    427    }, // append at the end
    428    {
    429      url: "https://site4.com",
    430      type: "bookmark",
    431      bookmarkGuid: "1234",
    432      date_added: Date.now(),
    433    }, // newest highlight
    434    {
    435      url: "https://site5.com",
    436      type: "bookmark",
    437      bookmarkGuid: "12345",
    438      date_added: Date.now() - 100,
    439    }, // 4th newest
    440    {
    441      url: "https://site6.com",
    442      type: "bookmark",
    443      bookmarkGuid: "1234",
    444      date_added: Date.now() - 40,
    445    }, // 2nd newest
    446  ];
    447  let expectedChronological = [4, 6, 0, 5];
    448  let expectedHistory = [1, 2, 3];
    449  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    450 
    451  let highlights = await fetchHighlightsRows(feed);
    452 
    453  [...expectedChronological, ...expectedHistory].forEach((link, index) => {
    454    Assert.equal(
    455      highlights[index].url,
    456      links[link].url,
    457      `highlight[${index}] should be link[${link}]`
    458    );
    459  });
    460 
    461  sandbox.restore();
    462 });
    463 
    464 add_task(async function test_fetchHighlights_TopSites_not_enabled() {
    465  info(
    466    "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " +
    467      "are not enabled"
    468  );
    469  let sandbox = sinon.createSandbox();
    470  let feed = getHighlightsFeedForTest(sandbox);
    471 
    472  sandbox.spy(feed.linksCache, "request");
    473 
    474  feed.store.state.Prefs.values["feeds.system.topsites"] = false;
    475 
    476  await feed.fetchHighlights();
    477 
    478  Assert.ok(
    479    NewTabUtils.activityStreamLinks.getHighlights.calledOnce,
    480    "NewTabUtils.activityStreamLinks.getHighlights was called"
    481  );
    482  Assert.ok(
    483    feed.linksCache.request.calledOnce,
    484    "HighlightsFeed.linksCache.request was called"
    485  );
    486 
    487  sandbox.restore();
    488 });
    489 
    490 add_task(async function test_fetchHighlights_TopSites_not_shown() {
    491  info(
    492    "HighlightsFeed.fetchHighlights should fetch Highlights if TopSites " +
    493      "are not shown on NTP"
    494  );
    495  let sandbox = sinon.createSandbox();
    496  let feed = getHighlightsFeedForTest(sandbox);
    497 
    498  sandbox.spy(feed.linksCache, "request");
    499 
    500  feed.store.state.Prefs.values["feeds.topsites"] = false;
    501 
    502  await feed.fetchHighlights();
    503 
    504  Assert.ok(
    505    NewTabUtils.activityStreamLinks.getHighlights.calledOnce,
    506    "NewTabUtils.activityStreamLinks.getHighlights was called"
    507  );
    508  Assert.ok(
    509    feed.linksCache.request.calledOnce,
    510    "HighlightsFeed.linksCache.request was called"
    511  );
    512 
    513  sandbox.restore();
    514 });
    515 
    516 add_task(async function test_fetchHighlights_add_hostname_hasImage() {
    517  info(
    518    "HighlightsFeed.fetchHighlights should add shortURL hostname and hasImage to each link"
    519  );
    520  let sandbox = sinon.createSandbox();
    521  let feed = getHighlightsFeedForTest(sandbox);
    522 
    523  let links = [{ url: "https://mozilla.org" }];
    524  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    525 
    526  let highlights = await fetchHighlightsRows(feed);
    527 
    528  Assert.equal(highlights[0].hostname, NewTabUtils.shortURL(links[0]));
    529  Assert.equal(highlights[0].hasImage, true);
    530 
    531  sandbox.restore();
    532 });
    533 
    534 add_task(async function test_fetchHighlights_add_existing_image() {
    535  info(
    536    "HighlightsFeed.fetchHighlights should add an existing image if it " +
    537      "exists to the link without calling fetchImage"
    538  );
    539  let sandbox = sinon.createSandbox();
    540  let feed = getHighlightsFeedForTest(sandbox);
    541 
    542  let links = [{ url: "https://mozilla.org", image: FAKE_IMAGE }];
    543  sandbox.spy(feed, "fetchImage");
    544  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    545 
    546  let highlights = await fetchHighlightsRows(feed);
    547 
    548  Assert.equal(highlights[0].image, FAKE_IMAGE);
    549  Assert.ok(feed.fetchImage.notCalled, "HighlightsFeed.fetchImage not called");
    550 
    551  sandbox.restore();
    552 });
    553 
    554 add_task(async function test_fetchHighlights_correct_args() {
    555  info(
    556    "HighlightsFeed.fetchHighlights should call fetchImage with the correct " +
    557      "arguments for new links"
    558  );
    559  let sandbox = sinon.createSandbox();
    560  let feed = getHighlightsFeedForTest(sandbox);
    561 
    562  let links = [
    563    {
    564      url: "https://mozilla.org",
    565      preview_image_url: "https://mozilla.org/preview.jog",
    566    },
    567  ];
    568  sandbox.spy(feed, "fetchImage");
    569  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    570 
    571  await fetchHighlightsRows(feed);
    572 
    573  Assert.ok(feed.fetchImage.calledOnce, "HighlightsFeed.fetchImage called");
    574 
    575  let [arg] = feed.fetchImage.firstCall.args;
    576  Assert.equal(arg.url, links[0].url);
    577  Assert.equal(arg.preview_image_url, links[0].preview_image_url);
    578 
    579  sandbox.restore();
    580 });
    581 
    582 add_task(
    583  async function test_fetchHighlights_not_include_links_already_in_TopSites() {
    584    info(
    585      "HighlightsFeed.fetchHighlights should not include any links already in " +
    586        "Top Sites"
    587    );
    588    let sandbox = sinon.createSandbox();
    589    let feed = getHighlightsFeedForTest(sandbox);
    590 
    591    let links = [
    592      { url: "https://mozilla.org" },
    593      { url: "http://www.topsite0.com" },
    594      { url: "http://www.topsite1.com" },
    595      { url: "http://www.topsite2.com" },
    596    ];
    597    NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    598 
    599    let highlights = await fetchHighlightsRows(feed);
    600 
    601    Assert.equal(highlights.length, 1);
    602    Assert.equal(highlights[0].url, links[0].url);
    603 
    604    sandbox.restore();
    605  }
    606 );
    607 
    608 add_task(
    609  async function test_fetchHighlights_not_include_history_already_in_TopSites() {
    610    info(
    611      "HighlightsFeed.fetchHighlights should include bookmark but not " +
    612        "history already in Top Sites"
    613    );
    614    let sandbox = sinon.createSandbox();
    615    let feed = getHighlightsFeedForTest(sandbox);
    616 
    617    let links = [
    618      { url: "http://www.topsite0.com", type: "bookmark" },
    619      { url: "http://www.topsite1.com", type: "history" },
    620    ];
    621    NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    622 
    623    let highlights = await fetchHighlightsRows(feed);
    624 
    625    Assert.equal(highlights.length, 1);
    626    Assert.equal(highlights[0].url, links[0].url);
    627 
    628    sandbox.restore();
    629  }
    630 );
    631 
    632 add_task(
    633  async function test_fetchHighlights_not_include_history_same_hostname_as_bookmark() {
    634    info(
    635      "HighlightsFeed.fetchHighlights should not include history of same " +
    636        "hostname as a bookmark"
    637    );
    638    let sandbox = sinon.createSandbox();
    639    let feed = getHighlightsFeedForTest(sandbox);
    640 
    641    let links = [
    642      { url: "https://site.com/bookmark", type: "bookmark" },
    643      { url: "https://site.com/history", type: "history" },
    644    ];
    645    NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    646 
    647    let highlights = await fetchHighlightsRows(feed);
    648 
    649    Assert.equal(highlights.length, 1);
    650    Assert.equal(highlights[0].url, links[0].url);
    651 
    652    sandbox.restore();
    653  }
    654 );
    655 
    656 add_task(async function test_fetchHighlights_take_first_history_of_hostname() {
    657  info(
    658    "HighlightsFeed.fetchHighlights should take the first history of a hostname"
    659  );
    660  let sandbox = sinon.createSandbox();
    661  let feed = getHighlightsFeedForTest(sandbox);
    662 
    663  let links = [
    664    { url: "https://site.com/first", type: "history" },
    665    { url: "https://site.com/second", type: "history" },
    666    { url: "https://other", type: "history" },
    667  ];
    668  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    669 
    670  let highlights = await fetchHighlightsRows(feed);
    671 
    672  Assert.equal(highlights.length, 2);
    673  Assert.equal(highlights[0].url, links[0].url);
    674  Assert.equal(highlights[1].url, links[2].url);
    675 
    676  sandbox.restore();
    677 });
    678 
    679 add_task(
    680  async function test_fetchHighlights_take_bookmark_pocket_download_of_same_hostname() {
    681    info(
    682      "HighlightsFeed.fetchHighlights should take a bookmark, a pocket, and " +
    683        "downloaded item of the same hostname"
    684    );
    685    let sandbox = sinon.createSandbox();
    686    let feed = getHighlightsFeedForTest(sandbox);
    687 
    688    let links = [
    689      { url: "https://site.com/bookmark", type: "bookmark" },
    690      { url: "https://site.com/pocket", type: "pocket" },
    691      { url: "https://site.com/download", type: "download" },
    692    ];
    693    NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    694 
    695    let highlights = await fetchHighlightsRows(feed);
    696 
    697    Assert.equal(highlights.length, 3);
    698    Assert.equal(highlights[0].url, links[0].url);
    699    Assert.equal(highlights[1].url, links[1].url);
    700    Assert.equal(highlights[2].url, links[2].url);
    701 
    702    sandbox.restore();
    703  }
    704 );
    705 
    706 add_task(async function test_fetchHighlights_do_not_include_downloads() {
    707  info(
    708    "HighlightsFeed.fetchHighlights should not include downloads when " +
    709      "includeDownloads pref is false"
    710  );
    711  let sandbox = sinon.createSandbox();
    712  let feed = getHighlightsFeedForTest(sandbox);
    713  feed.store.state.Prefs.values["section.highlights.includeDownloads"] = false;
    714  feed.downloadsManager.getDownloads = () => [
    715    { url: "https://site1.com/download" },
    716    { url: "https://site2.com/download" },
    717  ];
    718 
    719  let links = [
    720    { url: "https://site.com/bookmark", type: "bookmark" },
    721    { url: "https://site.com/pocket", type: "pocket" },
    722  ];
    723 
    724  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    725 
    726  let highlights = await fetchHighlightsRows(feed);
    727 
    728  Assert.equal(highlights.length, 2);
    729  Assert.equal(highlights[0].url, links[0].url);
    730  Assert.equal(highlights[1].url, links[1].url);
    731 
    732  sandbox.restore();
    733 });
    734 
    735 add_task(async function test_fetchHighlights_include_downloads() {
    736  info(
    737    "HighlightsFeed.fetchHighlights should include downloads when " +
    738      "includeDownloads pref is true"
    739  );
    740  let sandbox = sinon.createSandbox();
    741  let feed = getHighlightsFeedForTest(sandbox);
    742  feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
    743  feed.downloadsManager.getDownloads = () => [
    744    { url: "https://site.com/download" },
    745  ];
    746 
    747  let links = [
    748    { url: "https://site.com/bookmark", type: "bookmark" },
    749    { url: "https://site.com/pocket", type: "pocket" },
    750  ];
    751 
    752  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    753 
    754  let highlights = await fetchHighlightsRows(feed);
    755 
    756  Assert.equal(highlights.length, 3);
    757  Assert.equal(highlights[0].url, links[0].url);
    758  Assert.equal(highlights[1].url, links[1].url);
    759  Assert.equal(highlights[2].url, "https://site.com/download");
    760  Assert.equal(highlights[2].type, "download");
    761 
    762  sandbox.restore();
    763 });
    764 
    765 add_task(async function test_fetchHighlights_take_one_download() {
    766  info("HighlightsFeed.fetchHighlights should only take 1 download");
    767  let sandbox = sinon.createSandbox();
    768  let feed = getHighlightsFeedForTest(sandbox);
    769  feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
    770  feed.downloadsManager.getDownloads = () => [
    771    { url: "https://site1.com/download" },
    772    { url: "https://site2.com/download" },
    773  ];
    774 
    775  let links = [{ url: "https://site.com/bookmark", type: "bookmark" }];
    776 
    777  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    778 
    779  let highlights = await fetchHighlightsRows(feed);
    780 
    781  Assert.equal(highlights.length, 2);
    782  Assert.equal(highlights[0].url, links[0].url);
    783  Assert.equal(highlights[1].url, "https://site1.com/download");
    784 
    785  sandbox.restore();
    786 });
    787 
    788 add_task(async function test_fetchHighlights_chronological_sort() {
    789  info(
    790    "HighlightsFeed.fetchHighlights should sort bookmarks and downloads chronologically"
    791  );
    792  let sandbox = sinon.createSandbox();
    793  let feed = getHighlightsFeedForTest(sandbox);
    794  feed.store.state.Prefs.values["section.highlights.includeDownloads"] = true;
    795  feed.downloadsManager.getDownloads = () => [
    796    {
    797      url: "https://site1.com/download",
    798      type: "download",
    799      date_added: Date.now(),
    800    },
    801  ];
    802 
    803  let links = [
    804    {
    805      url: "https://site.com/bookmark",
    806      type: "bookmark",
    807      date_added: Date.now() - 10000,
    808    },
    809    {
    810      url: "https://site2.com/another-bookmark",
    811      type: "bookmark",
    812      date_added: Date.now() - 5000,
    813    },
    814    {
    815      url: "https://site3.com/visited",
    816      type: "history",
    817      date_added: Date.now(),
    818    },
    819  ];
    820 
    821  NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    822 
    823  let highlights = await fetchHighlightsRows(feed);
    824 
    825  Assert.equal(highlights.length, 4);
    826  Assert.equal(highlights[0].url, "https://site1.com/download");
    827  Assert.equal(highlights[1].url, links[1].url);
    828  Assert.equal(highlights[2].url, links[0].url);
    829  Assert.equal(highlights[3].url, links[2].url); // history item goes last
    830 
    831  sandbox.restore();
    832 });
    833 
    834 add_task(
    835  async function test_fetchHighlights_set_type_to_bookmark_on_bookmarkGuid() {
    836    info(
    837      "HighlightsFeed.fetchHighlights should set type to bookmark if there " +
    838        "is a bookmarkGuid"
    839    );
    840    let sandbox = sinon.createSandbox();
    841    let feed = getHighlightsFeedForTest(sandbox);
    842    feed.store.state.Prefs.values["section.highlights.includeBookmarks"] = true;
    843    feed.downloadsManager.getDownloads = () => [
    844      {
    845        url: "https://site1.com/download",
    846        type: "download",
    847        date_added: Date.now(),
    848      },
    849    ];
    850 
    851    let links = [
    852      {
    853        url: "https://mozilla.org",
    854        type: "history",
    855        bookmarkGuid: "1234567890",
    856      },
    857    ];
    858 
    859    NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    860 
    861    let highlights = await fetchHighlightsRows(feed);
    862 
    863    Assert.equal(highlights[0].type, "bookmark");
    864 
    865    sandbox.restore();
    866  }
    867 );
    868 
    869 add_task(
    870  async function test_fetchHighlights_keep_history_type_on_bookmarkGuid() {
    871    info(
    872      "HighlightsFeed.fetchHighlights should keep history type if there is a " +
    873        "bookmarkGuid but don't include bookmarks"
    874    );
    875    let sandbox = sinon.createSandbox();
    876    let feed = getHighlightsFeedForTest(sandbox);
    877    feed.store.state.Prefs.values["section.highlights.includeBookmarks"] =
    878      false;
    879 
    880    let links = [
    881      {
    882        url: "https://mozilla.org",
    883        type: "history",
    884        bookmarkGuid: "1234567890",
    885      },
    886    ];
    887 
    888    NewTabUtils.activityStreamLinks.getHighlights.resolves(links);
    889 
    890    let highlights = await fetchHighlightsRows(feed);
    891 
    892    Assert.equal(highlights[0].type, "history");
    893 
    894    sandbox.restore();
    895  }
    896 );
    897 
    898 add_task(async function test_fetchHighlights_filter_adult() {
    899  info("HighlightsFeed.fetchHighlights should filter out adult pages");
    900  let sandbox = sinon.createSandbox();
    901  let feed = getHighlightsFeedForTest(sandbox);
    902 
    903  sandbox.stub(FilterAdult, "filter").returns([]);
    904  let highlights = await fetchHighlightsRows(feed);
    905 
    906  Assert.ok(FilterAdult.filter.calledOnce, "FilterAdult.filter called");
    907  Assert.equal(highlights.length, 0);
    908 
    909  sandbox.restore();
    910 });
    911 
    912 add_task(async function test_fetchHighlights_no_expose_internal_link_props() {
    913  info(
    914    "HighlightsFeed.fetchHighlights should not expose internal link properties"
    915  );
    916  let sandbox = sinon.createSandbox();
    917  let feed = getHighlightsFeedForTest(sandbox);
    918 
    919  let highlights = await fetchHighlightsRows(feed);
    920  let internal = Object.keys(highlights[0]).filter(key => key.startsWith("__"));
    921 
    922  Assert.equal(internal.join(""), "");
    923 
    924  sandbox.restore();
    925 });
    926 
    927 add_task(
    928  async function test_fetchHighlights_broadcast_when_feed_not_initialized() {
    929    info(
    930      "HighlightsFeed.fetchHighlights should broadcast if feed is not initialized"
    931    );
    932    let sandbox = sinon.createSandbox();
    933    let feed = getHighlightsFeedForTest(sandbox);
    934 
    935    NewTabUtils.activityStreamLinks.getHighlights.resolves([]);
    936    sandbox.stub(SectionsManager, "updateSection");
    937    await feed.fetchHighlights();
    938 
    939    Assert.ok(
    940      SectionsManager.updateSection.calledOnce,
    941      "SectionsManager.updateSection called once"
    942    );
    943    Assert.ok(
    944      SectionsManager.updateSection.calledWithExactly(
    945        SECTION_ID,
    946        { rows: [] },
    947        true,
    948        undefined
    949      )
    950    );
    951    sandbox.restore();
    952  }
    953 );
    954 
    955 add_task(
    956  async function test_fetchHighlights_broadcast_on_broadcast_in_options() {
    957    info(
    958      "HighlightsFeed.fetchHighlights should broadcast if options.broadcast is true"
    959    );
    960    let sandbox = sinon.createSandbox();
    961    let feed = getHighlightsFeedForTest(sandbox);
    962 
    963    NewTabUtils.activityStreamLinks.getHighlights.resolves([]);
    964    feed.store.state.Sections[0].initialized = true;
    965 
    966    sandbox.stub(SectionsManager, "updateSection");
    967    await feed.fetchHighlights({ broadcast: true });
    968 
    969    Assert.ok(
    970      SectionsManager.updateSection.calledOnce,
    971      "SectionsManager.updateSection called once"
    972    );
    973    Assert.ok(
    974      SectionsManager.updateSection.calledWithExactly(
    975        SECTION_ID,
    976        { rows: [] },
    977        true,
    978        undefined
    979      )
    980    );
    981    sandbox.restore();
    982  }
    983 );
    984 
    985 add_task(async function test_fetchHighlights_no_broadcast() {
    986  info(
    987    "HighlightsFeed.fetchHighlights should not broadcast if " +
    988      "options.broadcast is false and initialized is true"
    989  );
    990  let sandbox = sinon.createSandbox();
    991  let feed = getHighlightsFeedForTest(sandbox);
    992 
    993  NewTabUtils.activityStreamLinks.getHighlights.resolves([]);
    994  feed.store.state.Sections[0].initialized = true;
    995 
    996  sandbox.stub(SectionsManager, "updateSection");
    997  await feed.fetchHighlights({ broadcast: false });
    998 
    999  Assert.ok(
   1000    SectionsManager.updateSection.calledOnce,
   1001    "SectionsManager.updateSection called once"
   1002  );
   1003  Assert.ok(
   1004    SectionsManager.updateSection.calledWithExactly(
   1005      SECTION_ID,
   1006      { rows: [] },
   1007      false,
   1008      undefined
   1009    )
   1010  );
   1011  sandbox.restore();
   1012 });
   1013 
   1014 add_task(async function test_fetchImage_capture_if_available() {
   1015  info("HighlightsFeed.fetchImage should capture the image, if available");
   1016  let sandbox = sinon.createSandbox();
   1017  let feed = getHighlightsFeedForTest(sandbox);
   1018 
   1019  sandbox.stub(Screenshots, "getScreenshotForURL");
   1020  sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
   1021 
   1022  await fetchImage(feed, {
   1023    preview_image_url: FAKE_IMAGE_URL,
   1024    url: FAKE_URL,
   1025  });
   1026 
   1027  Assert.ok(
   1028    Screenshots.getScreenshotForURL.calledOnce,
   1029    "Screenshots.getScreenshotForURL called once"
   1030  );
   1031  Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_IMAGE_URL));
   1032 
   1033  sandbox.restore();
   1034 });
   1035 
   1036 add_task(async function test_fetchImage_fallback_to_screenshot() {
   1037  info("HighlightsFeed.fetchImage should fall back to capturing a screenshot");
   1038  let sandbox = sinon.createSandbox();
   1039  let feed = getHighlightsFeedForTest(sandbox);
   1040 
   1041  sandbox.stub(Screenshots, "getScreenshotForURL");
   1042  sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
   1043 
   1044  await fetchImage(feed, { url: FAKE_URL });
   1045 
   1046  Assert.ok(
   1047    Screenshots.getScreenshotForURL.calledOnce,
   1048    "Screenshots.getScreenshotForURL called once"
   1049  );
   1050  Assert.ok(Screenshots.getScreenshotForURL.calledWith(FAKE_URL));
   1051 
   1052  sandbox.restore();
   1053 });
   1054 
   1055 add_task(async function test_fetchImage_updateSectionCard_args() {
   1056  info(
   1057    "HighlightsFeed.fetchImage should call " +
   1058      "SectionsManager.updateSectionCard with the right arguments"
   1059  );
   1060  let sandbox = sinon.createSandbox();
   1061  let feed = getHighlightsFeedForTest(sandbox);
   1062 
   1063  sandbox.stub(SectionsManager, "updateSectionCard");
   1064  sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE);
   1065  sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
   1066 
   1067  await fetchImage(feed, {
   1068    preview_image_url: FAKE_IMAGE_URL,
   1069    url: FAKE_URL,
   1070  });
   1071  Assert.ok(
   1072    SectionsManager.updateSectionCard.calledOnce,
   1073    "SectionsManager.updateSectionCard called"
   1074  );
   1075  Assert.ok(
   1076    SectionsManager.updateSectionCard.calledWith(
   1077      "highlights",
   1078      FAKE_URL,
   1079      { image: FAKE_IMAGE },
   1080      true
   1081    )
   1082  );
   1083  sandbox.restore();
   1084 });
   1085 
   1086 add_task(async function test_fetchImage_no_update_card_with_image() {
   1087  info("HighlightsFeed.fetchImage should not update the card with the image");
   1088  let sandbox = sinon.createSandbox();
   1089  let feed = getHighlightsFeedForTest(sandbox);
   1090 
   1091  sandbox.stub(SectionsManager, "updateSectionCard");
   1092  sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_IMAGE);
   1093  sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
   1094 
   1095  let card = {
   1096    preview_image_url: FAKE_IMAGE_URL,
   1097    url: FAKE_URL,
   1098  };
   1099  await fetchImage(feed, card);
   1100  Assert.ok(!card.image, "Image not set on card");
   1101  sandbox.restore();
   1102 });
   1103 
   1104 add_task(async function test_uninit_disable_section() {
   1105  info("HighlightsFeed.onAction(UNINIT) should disable its section");
   1106  let sandbox = sinon.createSandbox();
   1107  let feed = getHighlightsFeedForTest(sandbox);
   1108  feed.init();
   1109 
   1110  sandbox.stub(SectionsManager, "disableSection");
   1111  feed.onAction({ type: actionTypes.UNINIT });
   1112  Assert.ok(
   1113    SectionsManager.disableSection.calledOnce,
   1114    "SectionsManager.disableSection called"
   1115  );
   1116  Assert.ok(SectionsManager.disableSection.calledWith(SECTION_ID));
   1117  sandbox.restore();
   1118 });
   1119 
   1120 add_task(async function test_uninit_remove_expiration_filter() {
   1121  info("HighlightsFeed.onAction(UNINIT) should remove the expiration filter");
   1122  let sandbox = sinon.createSandbox();
   1123  let feed = getHighlightsFeedForTest(sandbox);
   1124  feed.init();
   1125 
   1126  sandbox.stub(PageThumbs, "removeExpirationFilter");
   1127  feed.onAction({ type: actionTypes.UNINIT });
   1128  Assert.ok(
   1129    PageThumbs.removeExpirationFilter.calledOnce,
   1130    "PageThumbs.removeExpirationFilter called"
   1131  );
   1132 
   1133  sandbox.restore();
   1134 });
   1135 
   1136 add_task(async function test_onAction_relay_to_DownloadsManager_onAction() {
   1137  info(
   1138    "HighlightsFeed.onAction should relay all actions to " +
   1139      "DownloadsManager.onAction"
   1140  );
   1141  let sandbox = sinon.createSandbox();
   1142  let feed = getHighlightsFeedForTest(sandbox);
   1143  sandbox.stub(feed.downloadsManager, "onAction");
   1144 
   1145  let action = {
   1146    type: actionTypes.COPY_DOWNLOAD_LINK,
   1147    data: { url: "foo.png" },
   1148    _target: {},
   1149  };
   1150  feed.onAction(action);
   1151 
   1152  Assert.ok(
   1153    feed.downloadsManager.onAction.calledOnce,
   1154    "HighlightsFeed.downloadManager.onAction called"
   1155  );
   1156  Assert.ok(feed.downloadsManager.onAction.calledWith(action));
   1157  sandbox.restore();
   1158 });
   1159 
   1160 add_task(async function test_onAction_fetch_highlights_on_SYSTEM_TICK() {
   1161  info("HighlightsFeed.onAction should fetch highlights on SYSTEM_TICK");
   1162  let sandbox = sinon.createSandbox();
   1163  let feed = getHighlightsFeedForTest(sandbox);
   1164 
   1165  await feed.fetchHighlights();
   1166 
   1167  sandbox.spy(feed, "fetchHighlights");
   1168  feed.onAction({ type: actionTypes.SYSTEM_TICK });
   1169 
   1170  Assert.ok(
   1171    feed.fetchHighlights.calledOnce,
   1172    "HighlightsFeed.fetchHighlights called"
   1173  );
   1174  Assert.ok(
   1175    feed.fetchHighlights.calledWithExactly({
   1176      broadcast: false,
   1177      isStartup: false,
   1178    })
   1179  );
   1180  sandbox.restore();
   1181 });
   1182 
   1183 add_task(
   1184  async function test_onAction_fetch_highlights_on_PREF_CHANGED_for_include() {
   1185    info(
   1186      "HighlightsFeed.onAction should fetch highlights on PREF_CHANGED " +
   1187        "for include prefs"
   1188    );
   1189    let sandbox = sinon.createSandbox();
   1190    let feed = getHighlightsFeedForTest(sandbox);
   1191 
   1192    sandbox.spy(feed, "fetchHighlights");
   1193    feed.onAction({
   1194      type: actionTypes.PREF_CHANGED,
   1195      data: { name: "section.highlights.includeBookmarks" },
   1196    });
   1197 
   1198    Assert.ok(
   1199      feed.fetchHighlights.calledOnce,
   1200      "HighlightsFeed.fetchHighlights called"
   1201    );
   1202    Assert.ok(feed.fetchHighlights.calledWithExactly({ broadcast: true }));
   1203    sandbox.restore();
   1204  }
   1205 );
   1206 
   1207 add_task(
   1208  async function test_onAction_no_fetch_highlights_on_PREF_CHANGED_for_other() {
   1209    info(
   1210      "HighlightsFeed.onAction should not fetch highlights on PREF_CHANGED " +
   1211        "for other prefs"
   1212    );
   1213    let sandbox = sinon.createSandbox();
   1214    let feed = getHighlightsFeedForTest(sandbox);
   1215 
   1216    sandbox.spy(feed, "fetchHighlights");
   1217    feed.onAction({
   1218      type: actionTypes.PREF_CHANGED,
   1219      data: { name: "section.topstories.pocketCta" },
   1220    });
   1221 
   1222    Assert.ok(
   1223      feed.fetchHighlights.notCalled,
   1224      "HighlightsFeed.fetchHighlights not called"
   1225    );
   1226 
   1227    sandbox.restore();
   1228  }
   1229 );
   1230 
   1231 add_task(async function test_onAction_fetch_highlights_on_actions() {
   1232  info("HighlightsFeed.onAction should fetch highlights for various actions");
   1233 
   1234  let actions = [
   1235    {
   1236      actionType: "PLACES_HISTORY_CLEARED",
   1237      expectsExpire: false,
   1238      expectsBroadcast: true,
   1239    },
   1240    {
   1241      actionType: "DOWNLOAD_CHANGED",
   1242      expectsExpire: false,
   1243      expectsBroadcast: true,
   1244    },
   1245    {
   1246      actionType: "PLACES_LINKS_CHANGED",
   1247      expectsExpire: true,
   1248      expectsBroadcast: false,
   1249    },
   1250    {
   1251      actionType: "PLACES_LINK_BLOCKED",
   1252      expectsExpire: false,
   1253      expectsBroadcast: true,
   1254    },
   1255  ];
   1256  for (let action of actions) {
   1257    info(
   1258      `HighlightsFeed.onAction should fetch highlights on ${action.actionType}`
   1259    );
   1260    let sandbox = sinon.createSandbox();
   1261    let feed = getHighlightsFeedForTest(sandbox);
   1262 
   1263    await feed.fetchHighlights();
   1264    sandbox.spy(feed, "fetchHighlights");
   1265    sandbox.stub(feed.linksCache, "expire");
   1266 
   1267    feed.onAction({ type: actionTypes[action.actionType] });
   1268    Assert.ok(
   1269      feed.fetchHighlights.calledOnce,
   1270      "HighlightsFeed.fetchHighlights called"
   1271    );
   1272    Assert.ok(
   1273      feed.fetchHighlights.calledWith({ broadcast: action.expectsBroadcast })
   1274    );
   1275 
   1276    if (action.expectsExpire) {
   1277      Assert.ok(
   1278        feed.linksCache.expire.calledOnce,
   1279        "HighlightsFeed.linksCache.expire called"
   1280      );
   1281    }
   1282 
   1283    sandbox.restore();
   1284  }
   1285 });
   1286 
   1287 add_task(
   1288  async function test_onAction_fetch_highlights_no_broadcast_on_TOP_SITES_UPDATED() {
   1289    info(
   1290      "HighlightsFeed.onAction should fetch highlights with broadcast " +
   1291        "false on TOP_SITES_UPDATED"
   1292    );
   1293 
   1294    let sandbox = sinon.createSandbox();
   1295    let feed = getHighlightsFeedForTest(sandbox);
   1296 
   1297    await feed.fetchHighlights();
   1298    sandbox.spy(feed, "fetchHighlights");
   1299 
   1300    feed.onAction({ type: actionTypes.TOP_SITES_UPDATED });
   1301    Assert.ok(
   1302      feed.fetchHighlights.calledOnce,
   1303      "HighlightsFeed.fetchHighlights called"
   1304    );
   1305    Assert.ok(
   1306      feed.fetchHighlights.calledWithExactly({
   1307        broadcast: false,
   1308        isStartup: false,
   1309      })
   1310    );
   1311 
   1312    sandbox.restore();
   1313  }
   1314 );