tor-browser

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

test_TopSitesFeed.js (103472B)


      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  actionCreators: "resource://newtab/common/Actions.mjs",
      8  actionTypes: "resource://newtab/common/Actions.mjs",
      9  ContileIntegration: "resource://newtab/lib/TopSitesFeed.sys.mjs",
     10  DEFAULT_TOP_SITES: "resource://newtab/lib/TopSitesFeed.sys.mjs",
     11  FilterAdult: "resource:///modules/FilterAdult.sys.mjs",
     12  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
     13  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     14  ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs",
     15  PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
     16  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     17  sinon: "resource://testing-common/Sinon.sys.mjs",
     18  Screenshots: "resource://newtab/lib/Screenshots.sys.mjs",
     19  Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs",
     20  SearchService: "resource://gre/modules/SearchService.sys.mjs",
     21  TOP_SITES_DEFAULT_ROWS: "resource:///modules/topsites/constants.mjs",
     22  TOP_SITES_MAX_SITES_PER_ROW: "resource:///modules/topsites/constants.mjs",
     23  TopSitesFeed: "resource://newtab/lib/TopSitesFeed.sys.mjs",
     24 });
     25 
     26 const FAKE_FAVICON = "data987";
     27 const FAKE_FAVICON_SIZE = 128;
     28 const FAKE_FRECENCY = PlacesUtils.history.pageFrecencyThreshold(0, 2, false);
     29 const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW)
     30  .fill(null)
     31  .map((v, i) => ({
     32    frecency: FAKE_FRECENCY,
     33    url: `http://www.site${i}.com`,
     34  }));
     35 const FAKE_SCREENSHOT = "data123";
     36 const SEARCH_SHORTCUTS_EXPERIMENT_PREF = "improvesearch.topSiteSearchShortcuts";
     37 const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF =
     38  "improvesearch.topSiteSearchShortcuts.searchEngines";
     39 const SEARCH_SHORTCUTS_HAVE_PINNED_PREF =
     40  "improvesearch.topSiteSearchShortcuts.havePinned";
     41 const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
     42 const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
     43 const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
     44 
     45 // This pref controls how long the contile cache is valid for in seconds.
     46 const CONTILE_CACHE_VALID_FOR_SECONDS_PREF =
     47  "browser.topsites.contile.cacheValidFor";
     48 // This pref records when the last contile fetch occurred, as a UNIX timestamp
     49 // in seconds.
     50 const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch";
     51 
     52 function FakeTippyTopProvider() {}
     53 FakeTippyTopProvider.prototype = {
     54  async init() {
     55    this.initialized = true;
     56  },
     57  processSite(site) {
     58    return site;
     59  },
     60 };
     61 
     62 let gSearchServiceInitStub;
     63 let gGetTopSitesStub;
     64 
     65 function getTopSitesFeedForTest(sandbox) {
     66  sandbox.stub(ContileIntegration.prototype, "PersistentCache").returns({
     67    set: sandbox.stub(),
     68    get: sandbox.stub(),
     69  });
     70 
     71  let feed = new TopSitesFeed();
     72 
     73  feed.store = {
     74    dispatch: sinon.spy(),
     75    getState() {
     76      return this.state;
     77    },
     78    state: {
     79      Prefs: { values: { topSitesRows: 2 } },
     80      TopSites: { rows: Array(12).fill("site") },
     81    },
     82  };
     83 
     84  return feed;
     85 }
     86 
     87 add_setup(async () => {
     88  let sandbox = sinon.createSandbox();
     89  sandbox.stub(SearchService.prototype, "defaultEngine").get(() => {
     90    return { identifier: "ddg", searchUrlDomain: "duckduckgo.com" };
     91  });
     92 
     93  gGetTopSitesStub = sandbox
     94    .stub(NewTabUtils.activityStreamLinks, "getTopSites")
     95    .resolves(FAKE_LINKS);
     96 
     97  gSearchServiceInitStub = sandbox
     98    .stub(SearchService.prototype, "init")
     99    .resolves();
    100 
    101  sandbox.stub(NewTabUtils.activityStreamProvider, "_faviconBytesToDataURI");
    102 
    103  sandbox
    104    .stub(NewTabUtils.activityStreamProvider, "_addFavicons")
    105    .callsFake(l => {
    106      return Promise.resolve(
    107        l.map(link => {
    108          link.favicon = FAKE_FAVICON;
    109          link.faviconSize = FAKE_FAVICON_SIZE;
    110          return link;
    111        })
    112      );
    113    });
    114 
    115  sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_SCREENSHOT);
    116  sandbox.spy(Screenshots, "maybeCacheScreenshot");
    117  sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
    118 
    119  registerCleanupFunction(() => {
    120    sandbox.restore();
    121  });
    122 });
    123 
    124 add_task(async function test_construction() {
    125  let sandbox = sinon.createSandbox();
    126  sandbox.stub(ContileIntegration.prototype, "PersistentCache").returns({
    127    set: sandbox.stub(),
    128    get: sandbox.stub(),
    129  });
    130  let feed = new TopSitesFeed();
    131  Assert.ok(feed, "Could construct a TopSitesFeed");
    132  Assert.ok(feed._currentSearchHostname, "_currentSearchHostname defined");
    133  sandbox.restore();
    134 });
    135 
    136 add_task(async function test_refreshDefaults() {
    137  let sandbox = sinon.createSandbox();
    138  sandbox.stub(ContileIntegration.prototype, "PersistentCache").returns({
    139    set: sandbox.stub(),
    140    get: sandbox.stub(),
    141  });
    142  let feed = new TopSitesFeed();
    143  Assert.ok(
    144    !DEFAULT_TOP_SITES.length,
    145    "Should have 0 DEFAULT_TOP_SITES initially."
    146  );
    147 
    148  info("refreshDefaults should add defaults on PREFS_INITIAL_VALUES");
    149  feed.onAction({
    150    type: actionTypes.PREFS_INITIAL_VALUES,
    151    data: { "default.sites": "https://foo.com" },
    152  });
    153 
    154  Assert.equal(
    155    DEFAULT_TOP_SITES.length,
    156    1,
    157    "Should have 1 DEFAULT_TOP_SITES now."
    158  );
    159 
    160  // Reset the DEFAULT_TOP_SITES;
    161  DEFAULT_TOP_SITES.length = 0;
    162 
    163  info("refreshDefaults should add defaults on default.sites PREF_CHANGED");
    164  feed.onAction({
    165    type: actionTypes.PREF_CHANGED,
    166    data: { name: "default.sites", value: "https://foo.com" },
    167  });
    168 
    169  Assert.equal(
    170    DEFAULT_TOP_SITES.length,
    171    1,
    172    "Should have 1 DEFAULT_TOP_SITES now."
    173  );
    174 
    175  // Reset the DEFAULT_TOP_SITES;
    176  DEFAULT_TOP_SITES.length = 0;
    177 
    178  info("refreshDefaults should refresh on topSiteRows PREF_CHANGED");
    179  let refreshStub = sandbox.stub(feed, "refresh");
    180  feed.onAction({
    181    type: actionTypes.PREF_CHANGED,
    182    data: { name: "topSitesRows" },
    183  });
    184  Assert.ok(feed.refresh.calledOnce, "refresh called");
    185  refreshStub.restore();
    186 
    187  // Reset the DEFAULT_TOP_SITES;
    188  DEFAULT_TOP_SITES.length = 0;
    189 
    190  info("refreshDefaults should have default sites with .isDefault = true");
    191  feed.refreshDefaults("https://foo.com");
    192  Assert.equal(
    193    DEFAULT_TOP_SITES.length,
    194    1,
    195    "Should have a DEFAULT_TOP_SITES now."
    196  );
    197  Assert.ok(
    198    DEFAULT_TOP_SITES[0].isDefault,
    199    "Lone top site should be the default."
    200  );
    201 
    202  // Reset the DEFAULT_TOP_SITES;
    203  DEFAULT_TOP_SITES.length = 0;
    204 
    205  info("refreshDefaults should have default sites with appropriate hostname");
    206  feed.refreshDefaults("https://foo.com");
    207  Assert.equal(
    208    DEFAULT_TOP_SITES.length,
    209    1,
    210    "Should have a DEFAULT_TOP_SITES now."
    211  );
    212  let [site] = DEFAULT_TOP_SITES;
    213  Assert.equal(
    214    site.hostname,
    215    NewTabUtils.shortURL(site),
    216    "Lone top site should have the right hostname."
    217  );
    218 
    219  // Reset the DEFAULT_TOP_SITES;
    220  DEFAULT_TOP_SITES.length = 0;
    221 
    222  info("refreshDefaults should add no defaults on empty pref");
    223  feed.refreshDefaults("");
    224  Assert.equal(
    225    DEFAULT_TOP_SITES.length,
    226    0,
    227    "Should have 0 DEFAULT_TOP_SITES now."
    228  );
    229 
    230  info("refreshDefaults should be able to clear defaults");
    231  feed.refreshDefaults("https://foo.com");
    232  feed.refreshDefaults("");
    233 
    234  Assert.equal(
    235    DEFAULT_TOP_SITES.length,
    236    0,
    237    "Should have 0 DEFAULT_TOP_SITES now."
    238  );
    239 
    240  sandbox.restore();
    241 });
    242 
    243 add_task(async function test_filterForThumbnailExpiration() {
    244  let sandbox = sinon.createSandbox();
    245  let feed = getTopSitesFeedForTest(sandbox);
    246 
    247  info(
    248    "filterForThumbnailExpiration should pass rows.urls to the callback provided"
    249  );
    250  const rows = [
    251    { url: "foo.com" },
    252    { url: "bar.com", customScreenshotURL: "custom" },
    253  ];
    254  feed.store.state.TopSites = { rows };
    255  const stub = sandbox.stub();
    256  feed.filterForThumbnailExpiration(stub);
    257  Assert.ok(stub.calledOnce);
    258  Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "custom"]));
    259 
    260  sandbox.restore();
    261 });
    262 
    263 add_task(
    264  async function test_getLinksWithDefaults_on_SearchService_init_failure() {
    265    let sandbox = sinon.createSandbox();
    266    let feed = getTopSitesFeedForTest(sandbox);
    267 
    268    feed.refreshDefaults("https://foo.com");
    269 
    270    gSearchServiceInitStub.rejects(new Error("Simulating search init failure"));
    271 
    272    const result = await feed.getLinksWithDefaults();
    273    Assert.ok(result);
    274 
    275    gSearchServiceInitStub.resolves();
    276 
    277    sandbox.restore();
    278  }
    279 );
    280 
    281 add_task(async function test_getLinksWithDefaults() {
    282  NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
    283 
    284  let sandbox = sinon.createSandbox();
    285  let feed = getTopSitesFeedForTest(sandbox);
    286 
    287  feed.refreshDefaults("https://foo.com");
    288 
    289  info("getLinksWithDefaults should get the links from NewTabUtils");
    290  let result = await feed.getLinksWithDefaults();
    291 
    292  const reference = FAKE_LINKS.map(site =>
    293    Object.assign({}, site, {
    294      hostname: NewTabUtils.shortURL(site),
    295      typedBonus: true,
    296    })
    297  );
    298 
    299  Assert.deepEqual(result, reference);
    300  Assert.ok(NewTabUtils.activityStreamLinks.getTopSites.calledOnce);
    301 
    302  info("getLinksWithDefaults should indicate the links get typed bonus");
    303  Assert.ok(result[0].typedBonus, "Expected typed bonus property to be true.");
    304 
    305  sandbox.restore();
    306 });
    307 
    308 add_task(async function test_getLinksWithDefaults_filterAdult() {
    309  let sandbox = sinon.createSandbox();
    310  info("getLinksWithDefaults should filter out non-pinned adult sites");
    311 
    312  sandbox.stub(FilterAdult, "filter").returns([]);
    313  const TEST_URL = "https://foo.com/";
    314  sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [{ url: TEST_URL }]);
    315 
    316  let feed = getTopSitesFeedForTest(sandbox);
    317  feed.refreshDefaults("https://foo.com");
    318 
    319  const result = await feed.getLinksWithDefaults();
    320  Assert.ok(FilterAdult.filter.calledOnce);
    321  Assert.equal(result.length, 1);
    322  Assert.equal(result[0].url, TEST_URL);
    323 
    324  sandbox.restore();
    325 });
    326 
    327 add_task(async function test_getLinksWithDefaults_caching() {
    328  let sandbox = sinon.createSandbox();
    329 
    330  info(
    331    "getLinksWithDefaults should filter out the defaults that have been blocked"
    332  );
    333  // make sure we only have one top site, and we block the only default site we have to show
    334  const url = "www.myonlytopsite.com";
    335  const topsite = {
    336    frecency: FAKE_FRECENCY,
    337    hostname: NewTabUtils.shortURL({ url }),
    338    typedBonus: true,
    339    url,
    340  };
    341 
    342  const blockedDefaultSite = { url: "https://foo.com" };
    343  gGetTopSitesStub.resolves([topsite]);
    344  sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
    345    return site.url === blockedDefaultSite.url;
    346  });
    347 
    348  let feed = getTopSitesFeedForTest(sandbox);
    349  feed.refreshDefaults("https://foo.com");
    350  const result = await feed.getLinksWithDefaults();
    351 
    352  // what we should be left with is just the top site we added, and not the default site we blocked
    353  Assert.equal(result.length, 1);
    354  Assert.deepEqual(result[0], topsite);
    355  let foundBlocked = result.find(site => site.url === blockedDefaultSite.url);
    356  Assert.ok(!foundBlocked, "Should not have found blocked site.");
    357 
    358  gGetTopSitesStub.resolves(FAKE_LINKS);
    359  sandbox.restore();
    360 });
    361 
    362 add_task(async function test_getLinksWithDefaults_dedupe() {
    363  let sandbox = sinon.createSandbox();
    364 
    365  info("getLinksWithDefaults should call dedupe.group on the links");
    366  let feed = getTopSitesFeedForTest(sandbox);
    367  feed.refreshDefaults("https://foo.com");
    368 
    369  let stub = sandbox.stub(feed.dedupe, "group").callsFake((...id) => id);
    370  await feed.getLinksWithDefaults();
    371 
    372  Assert.ok(stub.calledOnce, "dedupe.group was called once");
    373  sandbox.restore();
    374 });
    375 
    376 add_task(async function test__dedupe_key() {
    377  let sandbox = sinon.createSandbox();
    378 
    379  info("_dedupeKey should dedupe on hostname instead of url");
    380  let feed = getTopSitesFeedForTest(sandbox);
    381  feed.refreshDefaults("https://foo.com");
    382 
    383  let site = { url: "foo", hostname: "bar" };
    384  let result = feed._dedupeKey(site);
    385 
    386  Assert.equal(result, site.hostname, "deduped on hostname");
    387  sandbox.restore();
    388 });
    389 
    390 add_task(async function test_getLinksWithDefaults_adds_defaults() {
    391  let sandbox = sinon.createSandbox();
    392 
    393  info(
    394    "getLinksWithDefaults should add defaults if there are are not enough links"
    395  );
    396  const TEST_LINKS = [{ frecency: FAKE_FRECENCY, url: "foo.com" }];
    397  gGetTopSitesStub.resolves(TEST_LINKS);
    398  let feed = getTopSitesFeedForTest(sandbox);
    399  feed.refreshDefaults("https://foo.com");
    400 
    401  let result = await feed.getLinksWithDefaults();
    402 
    403  let reference = [...TEST_LINKS, ...DEFAULT_TOP_SITES].map(s =>
    404    Object.assign({}, s, {
    405      hostname: NewTabUtils.shortURL(s),
    406      typedBonus: true,
    407    })
    408  );
    409 
    410  Assert.deepEqual(result, reference);
    411 
    412  gGetTopSitesStub.resolves(FAKE_LINKS);
    413  sandbox.restore();
    414 });
    415 
    416 add_task(
    417  async function test_getLinksWithDefaults_adds_defaults_for_visible_slots() {
    418    let sandbox = sinon.createSandbox();
    419 
    420    info(
    421      "getLinksWithDefaults should only add defaults up to the number of visible slots"
    422    );
    423    const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
    424    let testLinks = [];
    425    for (let i = 0; i < numVisible - 1; i++) {
    426      testLinks.push({ frecency: FAKE_FRECENCY, url: `foo${i}.com` });
    427    }
    428    gGetTopSitesStub.resolves(testLinks);
    429 
    430    let feed = getTopSitesFeedForTest(sandbox);
    431    feed.refreshDefaults("https://foo.com");
    432 
    433    let result = await feed.getLinksWithDefaults();
    434 
    435    let reference = [...testLinks, DEFAULT_TOP_SITES[0]].map(s =>
    436      Object.assign({}, s, {
    437        hostname: NewTabUtils.shortURL(s),
    438        typedBonus: true,
    439      })
    440    );
    441 
    442    Assert.equal(result.length, numVisible);
    443    Assert.deepEqual(result, reference);
    444 
    445    gGetTopSitesStub.resolves(FAKE_LINKS);
    446    sandbox.restore();
    447  }
    448 );
    449 
    450 add_task(async function test_getLinksWithDefaults_no_throw_on_no_links() {
    451  let sandbox = sinon.createSandbox();
    452 
    453  info("getLinksWithDefaults should not throw if NewTabUtils returns null");
    454  gGetTopSitesStub.resolves(null);
    455 
    456  let feed = getTopSitesFeedForTest(sandbox);
    457  feed.refreshDefaults("https://foo.com");
    458 
    459  feed.getLinksWithDefaults();
    460  Assert.ok(true, "getLinksWithDefaults did not throw");
    461 
    462  gGetTopSitesStub.resolves(FAKE_LINKS);
    463  sandbox.restore();
    464 });
    465 
    466 add_task(async function test_getLinksWithDefaults_get_more_on_request() {
    467  let sandbox = sinon.createSandbox();
    468 
    469  info("getLinksWithDefaults should get more if the user has asked for more");
    470  let testLinks = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW)
    471    .fill(null)
    472    .map((v, i) => ({
    473      frecency: FAKE_FRECENCY,
    474      url: `http://www.site${i}.com`,
    475    }));
    476  gGetTopSitesStub.resolves(testLinks);
    477 
    478  let feed = getTopSitesFeedForTest(sandbox);
    479  feed.refreshDefaults("https://foo.com");
    480 
    481  const TEST_ROWS = 3;
    482  feed.store.state.Prefs.values.topSitesRows = TEST_ROWS;
    483 
    484  let result = await feed.getLinksWithDefaults();
    485  Assert.equal(result.length, TEST_ROWS * TOP_SITES_MAX_SITES_PER_ROW);
    486 
    487  gGetTopSitesStub.resolves(FAKE_LINKS);
    488  sandbox.restore();
    489 });
    490 
    491 add_task(async function test_getLinksWithDefaults_reuse_cache() {
    492  let sandbox = sinon.createSandbox();
    493  info("getLinksWithDefaults should reuse the cache on subsequent calls");
    494 
    495  let feed = getTopSitesFeedForTest(sandbox);
    496  feed.refreshDefaults("https://foo.com");
    497 
    498  gGetTopSitesStub.resetHistory();
    499 
    500  await feed.getLinksWithDefaults();
    501  await feed.getLinksWithDefaults();
    502 
    503  Assert.ok(
    504    NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
    505    "getTopSites only called once"
    506  );
    507 
    508  sandbox.restore();
    509 });
    510 
    511 add_task(
    512  async function test_getLinksWithDefaults_ignore_cache_on_requesting_more() {
    513    let sandbox = sinon.createSandbox();
    514    info("getLinksWithDefaults should ignore the cache when requesting more");
    515 
    516    let feed = getTopSitesFeedForTest(sandbox);
    517    feed.refreshDefaults("https://foo.com");
    518 
    519    gGetTopSitesStub.resetHistory();
    520 
    521    await feed.getLinksWithDefaults();
    522    feed.store.state.Prefs.values.topSitesRows *= 100;
    523    await feed.getLinksWithDefaults();
    524 
    525    Assert.ok(
    526      NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
    527      "getTopSites called twice"
    528    );
    529 
    530    sandbox.restore();
    531  }
    532 );
    533 
    534 add_task(
    535  async function test_getLinksWithDefaults_migrate_frecent_screenshot_data() {
    536    let sandbox = sinon.createSandbox();
    537    info(
    538      "getLinksWithDefaults should migrate frecent screenshot data without getting screenshots again"
    539    );
    540 
    541    let feed = getTopSitesFeedForTest(sandbox);
    542    feed.refreshDefaults("https://foo.com");
    543 
    544    gGetTopSitesStub.resetHistory();
    545 
    546    feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
    547    await feed.getLinksWithDefaults();
    548 
    549    let originalCallCount = Screenshots.getScreenshotForURL.callCount;
    550    feed.frecentCache.expire();
    551 
    552    let result = await feed.getLinksWithDefaults();
    553 
    554    Assert.ok(
    555      NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
    556      "getTopSites called twice"
    557    );
    558    Assert.equal(
    559      Screenshots.getScreenshotForURL.callCount,
    560      originalCallCount,
    561      "getScreenshotForURL was not called again."
    562    );
    563    Assert.equal(result[0].screenshot, FAKE_SCREENSHOT);
    564 
    565    sandbox.restore();
    566  }
    567 );
    568 
    569 add_task(
    570  async function test_getLinksWithDefaults_migrate_pinned_favicon_data() {
    571    let sandbox = sinon.createSandbox();
    572    info(
    573      "getLinksWithDefaults should migrate pinned favicon data without getting favicons again"
    574    );
    575 
    576    let feed = getTopSitesFeedForTest(sandbox);
    577    feed.refreshDefaults("https://foo.com");
    578 
    579    gGetTopSitesStub.resetHistory();
    580 
    581    sandbox
    582      .stub(NewTabUtils.pinnedLinks, "links")
    583      .get(() => [{ url: "https://foo.com/" }]);
    584 
    585    await feed.getLinksWithDefaults();
    586 
    587    let originalCallCount =
    588      NewTabUtils.activityStreamProvider._addFavicons.callCount;
    589    feed.pinnedCache.expire();
    590 
    591    let result = await feed.getLinksWithDefaults();
    592 
    593    Assert.equal(
    594      NewTabUtils.activityStreamProvider._addFavicons.callCount,
    595      originalCallCount,
    596      "_addFavicons was not called again."
    597    );
    598    Assert.equal(result[0].favicon, FAKE_FAVICON);
    599    Assert.equal(result[0].faviconSize, FAKE_FAVICON_SIZE);
    600 
    601    sandbox.restore();
    602  }
    603 );
    604 
    605 add_task(async function test_getLinksWithDefaults_no_internal_properties() {
    606  let sandbox = sinon.createSandbox();
    607  info("getLinksWithDefaults should not expose internal link properties");
    608 
    609  let feed = getTopSitesFeedForTest(sandbox);
    610  feed.refreshDefaults("https://foo.com");
    611 
    612  let result = await feed.getLinksWithDefaults();
    613 
    614  let internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
    615  Assert.equal(internal.join(""), "");
    616 
    617  sandbox.restore();
    618 });
    619 
    620 add_task(async function test_getLinksWithDefaults_copy_frecent_screenshot() {
    621  let sandbox = sinon.createSandbox();
    622  info(
    623    "getLinksWithDefaults should copy the screenshot of the frecent site if " +
    624      "pinned site doesn't have customScreenshotURL"
    625  );
    626 
    627  let feed = getTopSitesFeedForTest(sandbox);
    628  feed.refreshDefaults("https://foo.com");
    629 
    630  const TEST_SCREENSHOT = "screenshot";
    631 
    632  gGetTopSitesStub.resolves([
    633    { url: "https://foo.com/", screenshot: TEST_SCREENSHOT },
    634  ]);
    635  sandbox
    636    .stub(NewTabUtils.pinnedLinks, "links")
    637    .get(() => [{ url: "https://foo.com/" }]);
    638 
    639  let result = await feed.getLinksWithDefaults();
    640 
    641  Assert.equal(result[0].screenshot, TEST_SCREENSHOT);
    642 
    643  gGetTopSitesStub.resolves(FAKE_LINKS);
    644  sandbox.restore();
    645 });
    646 
    647 add_task(async function test_getLinksWithDefaults_no_copy_frecent_screenshot() {
    648  let sandbox = sinon.createSandbox();
    649  info(
    650    "getLinksWithDefaults should not copy the frecent screenshot if " +
    651      "customScreenshotURL is set"
    652  );
    653 
    654  let feed = getTopSitesFeedForTest(sandbox);
    655  feed.refreshDefaults("https://foo.com");
    656 
    657  gGetTopSitesStub.resolves([
    658    { url: "https://foo.com/", screenshot: "screenshot" },
    659  ]);
    660  sandbox
    661    .stub(NewTabUtils.pinnedLinks, "links")
    662    .get(() => [{ url: "https://foo.com/", customScreenshotURL: "custom" }]);
    663 
    664  let result = await feed.getLinksWithDefaults();
    665 
    666  Assert.equal(result[0].screenshot, undefined);
    667 
    668  gGetTopSitesStub.resolves(FAKE_LINKS);
    669  sandbox.restore();
    670 });
    671 
    672 add_task(async function test_getLinksWithDefaults_persist_screenshot() {
    673  let sandbox = sinon.createSandbox();
    674  info(
    675    "getLinksWithDefaults should keep the same screenshot if no frecent site is found"
    676  );
    677 
    678  let feed = getTopSitesFeedForTest(sandbox);
    679  feed.refreshDefaults("https://foo.com");
    680 
    681  const CUSTOM_SCREENSHOT = "custom";
    682 
    683  gGetTopSitesStub.resolves([]);
    684  sandbox
    685    .stub(NewTabUtils.pinnedLinks, "links")
    686    .get(() => [{ url: "https://foo.com/", screenshot: CUSTOM_SCREENSHOT }]);
    687 
    688  let result = await feed.getLinksWithDefaults();
    689 
    690  Assert.equal(result[0].screenshot, CUSTOM_SCREENSHOT);
    691 
    692  gGetTopSitesStub.resolves(FAKE_LINKS);
    693  sandbox.restore();
    694 });
    695 
    696 add_task(
    697  async function test_getLinksWithDefaults_no_overwrite_pinned_screenshot() {
    698    let sandbox = sinon.createSandbox();
    699    info("getLinksWithDefaults should not overwrite pinned site screenshot");
    700 
    701    let feed = getTopSitesFeedForTest(sandbox);
    702    feed.refreshDefaults("https://foo.com");
    703 
    704    const EXISTING_SCREENSHOT = "some-screenshot";
    705 
    706    gGetTopSitesStub.resolves([{ url: "https://foo.com/", screenshot: "foo" }]);
    707    sandbox
    708      .stub(NewTabUtils.pinnedLinks, "links")
    709      .get(() => [
    710        { url: "https://foo.com/", screenshot: EXISTING_SCREENSHOT },
    711      ]);
    712 
    713    let result = await feed.getLinksWithDefaults();
    714 
    715    Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
    716 
    717    gGetTopSitesStub.resolves(FAKE_LINKS);
    718    sandbox.restore();
    719  }
    720 );
    721 
    722 add_task(
    723  async function test_getLinksWithDefaults_no_searchTopSite_from_frecent() {
    724    let sandbox = sinon.createSandbox();
    725    info("getLinksWithDefaults should not set searchTopSite from frecent site");
    726 
    727    let feed = getTopSitesFeedForTest(sandbox);
    728    feed.refreshDefaults("https://foo.com");
    729 
    730    const EXISTING_SCREENSHOT = "some-screenshot";
    731 
    732    gGetTopSitesStub.resolves([
    733      {
    734        url: "https://foo.com/",
    735        searchTopSite: true,
    736        screenshot: EXISTING_SCREENSHOT,
    737      },
    738    ]);
    739    sandbox
    740      .stub(NewTabUtils.pinnedLinks, "links")
    741      .get(() => [{ url: "https://foo.com/" }]);
    742 
    743    let result = await feed.getLinksWithDefaults();
    744 
    745    Assert.ok(!result[0].searchTopSite);
    746    // But it should copy over other properties
    747    Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
    748 
    749    gGetTopSitesStub.resolves(FAKE_LINKS);
    750    sandbox.restore();
    751  }
    752 );
    753 
    754 add_task(async function test_getLinksWithDefaults_concurrency_getTopSites() {
    755  let sandbox = sinon.createSandbox();
    756  info(
    757    "getLinksWithDefaults concurrent calls should call the backing data once"
    758  );
    759 
    760  let feed = getTopSitesFeedForTest(sandbox);
    761  feed.refreshDefaults("https://foo.com");
    762 
    763  NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
    764 
    765  await Promise.all([feed.getLinksWithDefaults(), feed.getLinksWithDefaults()]);
    766 
    767  Assert.ok(
    768    NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
    769    "getTopSites only called once"
    770  );
    771 
    772  sandbox.restore();
    773 });
    774 
    775 add_task(
    776  async function test_getLinksWithDefaults_concurrency_getScreenshotForURL() {
    777    let sandbox = sinon.createSandbox();
    778    info(
    779      "getLinksWithDefaults concurrent calls should call the backing data once"
    780    );
    781 
    782    let feed = getTopSitesFeedForTest(sandbox);
    783    feed.refreshDefaults("https://foo.com");
    784    feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
    785 
    786    NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
    787    Screenshots.getScreenshotForURL.resetHistory();
    788 
    789    await Promise.all([
    790      feed.getLinksWithDefaults(),
    791      feed.getLinksWithDefaults(),
    792    ]);
    793 
    794    Assert.ok(
    795      NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
    796      "getTopSites only called once"
    797    );
    798 
    799    Assert.equal(
    800      Screenshots.getScreenshotForURL.callCount,
    801      FAKE_LINKS.length,
    802      "getLinksWithDefaults concurrent calls should get screenshots once per link"
    803    );
    804 
    805    sandbox.restore();
    806    feed = getTopSitesFeedForTest(sandbox);
    807    feed.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
    808 
    809    feed.refreshDefaults("https://foo.com");
    810 
    811    sandbox.stub(feed, "_requestRichIcon");
    812    await Promise.all([
    813      feed.getLinksWithDefaults(),
    814      feed.getLinksWithDefaults(),
    815    ]);
    816 
    817    Assert.equal(
    818      feed.store.dispatch.callCount,
    819      FAKE_LINKS.length,
    820      "getLinksWithDefaults concurrent calls should dispatch once per link screenshot fetched"
    821    );
    822 
    823    sandbox.restore();
    824  }
    825 );
    826 
    827 add_task(async function test_getLinksWithDefaults_deduping_no_dedupe_pinned() {
    828  let sandbox = sinon.createSandbox();
    829  info("getLinksWithDefaults should not dedupe pinned sites");
    830 
    831  let feed = getTopSitesFeedForTest(sandbox);
    832  feed.refreshDefaults("https://foo.com");
    833 
    834  sandbox
    835    .stub(NewTabUtils.pinnedLinks, "links")
    836    .get(() => [
    837      { url: "https://developer.mozilla.org/en-US/docs/Web" },
    838      { url: "https://developer.mozilla.org/en-US/docs/Learn" },
    839    ]);
    840 
    841  let sites = await feed.getLinksWithDefaults();
    842  Assert.equal(sites.length, 2 * TOP_SITES_MAX_SITES_PER_ROW);
    843  Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
    844  Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
    845  Assert.equal(sites[0].hostname, sites[1].hostname);
    846 
    847  sandbox.restore();
    848 });
    849 
    850 add_task(async function test_getLinksWithDefaults_prefer_pinned_sites() {
    851  let sandbox = sinon.createSandbox();
    852 
    853  info("getLinksWithDefaults should prefer pinned sites over links");
    854 
    855  let feed = getTopSitesFeedForTest(sandbox);
    856  feed.refreshDefaults();
    857 
    858  sandbox
    859    .stub(NewTabUtils.pinnedLinks, "links")
    860    .get(() => [
    861      { url: "https://developer.mozilla.org/en-US/docs/Web" },
    862      { url: "https://developer.mozilla.org/en-US/docs/Learn" },
    863    ]);
    864 
    865  const SECOND_TOP_SITE_URL = "https://www.mozilla.org/";
    866 
    867  gGetTopSitesStub.resolves([
    868    { frecency: FAKE_FRECENCY, url: "https://developer.mozilla.org/" },
    869    { frecency: FAKE_FRECENCY, url: SECOND_TOP_SITE_URL },
    870  ]);
    871 
    872  let sites = await feed.getLinksWithDefaults();
    873 
    874  // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so
    875  // the frecent with matching hostname as pinned is removed.
    876  Assert.equal(sites.length, 3);
    877  Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
    878  Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
    879  Assert.equal(sites[2].url, SECOND_TOP_SITE_URL);
    880 
    881  gGetTopSitesStub.resolves(FAKE_LINKS);
    882  sandbox.restore();
    883 });
    884 
    885 add_task(async function test_getLinksWithDefaults_title_and_null() {
    886  let sandbox = sinon.createSandbox();
    887 
    888  info("getLinksWithDefaults should return sites that have a title");
    889 
    890  let feed = getTopSitesFeedForTest(sandbox);
    891  feed.refreshDefaults();
    892 
    893  sandbox
    894    .stub(NewTabUtils.pinnedLinks, "links")
    895    .get(() => [{ url: "https://github.com/mozilla/activity-stream" }]);
    896 
    897  let sites = await feed.getLinksWithDefaults();
    898  for (let site of sites) {
    899    Assert.ok(site.hostname);
    900  }
    901 
    902  info("getLinksWithDefaults should not throw for null entries");
    903  sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [null]);
    904  await feed.getLinksWithDefaults();
    905  Assert.ok(true, "getLinksWithDefaults didn't throw");
    906 
    907  sandbox.restore();
    908 });
    909 
    910 add_task(async function test_getLinksWithDefaults_calls__fetchIcon() {
    911  let sandbox = sinon.createSandbox();
    912 
    913  info("getLinksWithDefaults should return sites that have a title");
    914 
    915  let feed = getTopSitesFeedForTest(sandbox);
    916  feed.refreshDefaults();
    917 
    918  sandbox.spy(feed, "_fetchIcon");
    919  let results = await feed.getLinksWithDefaults();
    920  Assert.ok(results.length, "Got back some results");
    921  Assert.equal(feed._fetchIcon.callCount, results.length);
    922  for (let result of results) {
    923    Assert.ok(feed._fetchIcon.calledWith(result));
    924  }
    925 
    926  sandbox.restore();
    927 });
    928 
    929 add_task(async function test_getLinksWithDefaults_calls__fetchScreenshot() {
    930  let sandbox = sinon.createSandbox();
    931 
    932  info(
    933    "getLinksWithDefaults should call _fetchScreenshot when customScreenshotURL is set"
    934  );
    935 
    936  gGetTopSitesStub.resolves([]);
    937  sandbox
    938    .stub(NewTabUtils.pinnedLinks, "links")
    939    .get(() => [{ url: "https://foo.com", customScreenshotURL: "custom" }]);
    940 
    941  let feed = getTopSitesFeedForTest(sandbox);
    942  feed.refreshDefaults();
    943 
    944  sandbox.stub(feed, "_fetchScreenshot");
    945  await feed.getLinksWithDefaults();
    946 
    947  Assert.ok(feed._fetchScreenshot.calledWith(sinon.match.object, "custom"));
    948 
    949  gGetTopSitesStub.resolves(FAKE_LINKS);
    950  sandbox.restore();
    951 });
    952 
    953 add_task(async function test_getLinksWithDefaults_with_DiscoveryStream() {
    954  let sandbox = sinon.createSandbox();
    955  info(
    956    "getLinksWithDefaults should add a sponsored topsite from discoverystream to all the valid indices"
    957  );
    958 
    959  let makeStreamData = index => ({
    960    layout: [
    961      {
    962        components: [
    963          {
    964            placement: {
    965              name: "sponsored-topsites",
    966            },
    967            spocs: {
    968              positions: [{ index }],
    969            },
    970          },
    971        ],
    972      },
    973    ],
    974    spocs: {
    975      data: {
    976        "sponsored-topsites": {
    977          items: [{ title: "test spoc", url: "https://test-spoc.com" }],
    978        },
    979      },
    980    },
    981  });
    982 
    983  let feed = getTopSitesFeedForTest(sandbox);
    984  feed.refreshDefaults();
    985 
    986  for (let i = 0; i < FAKE_LINKS.length; i++) {
    987    feed.store.state.DiscoveryStream = makeStreamData(i);
    988    const result = await feed.getLinksWithDefaults();
    989    const link = result[i];
    990 
    991    Assert.equal(link.type, "SPOC");
    992    Assert.equal(link.title, "test spoc");
    993    Assert.equal(link.sponsored_position, i + 1);
    994    Assert.equal(link.hostname, "test-spoc");
    995    Assert.equal(link.url, "https://test-spoc.com");
    996  }
    997 
    998  sandbox.restore();
    999 });
   1000 
   1001 add_task(async function test_init() {
   1002  let sandbox = sinon.createSandbox();
   1003 
   1004  sandbox.stub(NimbusFeatures.newtab, "onUpdate");
   1005 
   1006  let feed = getTopSitesFeedForTest(sandbox);
   1007 
   1008  sandbox.stub(feed, "refresh");
   1009  await feed.init();
   1010 
   1011  info("TopSitesFeed.init should call refresh (broadcast: true)");
   1012  Assert.ok(feed.refresh.calledOnce, "refresh called once");
   1013  Assert.ok(
   1014    feed.refresh.calledWithExactly({
   1015      broadcast: true,
   1016      isStartup: true,
   1017    })
   1018  );
   1019 
   1020  info(
   1021    "TopSitesFeed.init should call onUpdate to set up Nimbus update listener"
   1022  );
   1023 
   1024  Assert.ok(
   1025    NimbusFeatures.newtab.onUpdate.calledOnce,
   1026    "NimbusFeatures.newtab.onUpdate called once"
   1027  );
   1028  sandbox.restore();
   1029 });
   1030 
   1031 add_task(async function test_refresh() {
   1032  let sandbox = sinon.createSandbox();
   1033 
   1034  sandbox.stub(NimbusFeatures.newtab, "onUpdate");
   1035 
   1036  let feed = getTopSitesFeedForTest(sandbox);
   1037 
   1038  sandbox.stub(feed, "_fetchIcon");
   1039  feed._startedUp = true;
   1040 
   1041  info("TopSitesFeed.refresh should wait for tippytop to initialize");
   1042  feed._tippyTopProvider.initialized = false;
   1043  sandbox.stub(feed._tippyTopProvider, "init").resolves();
   1044 
   1045  await feed.refresh();
   1046 
   1047  Assert.ok(
   1048    feed._tippyTopProvider.init.calledOnce,
   1049    "feed._tippyTopProvider.init called once"
   1050  );
   1051 
   1052  info(
   1053    "TopSitesFeed.refresh should not init the tippyTopProvider if already initialized"
   1054  );
   1055  feed._tippyTopProvider.initialized = true;
   1056  feed._tippyTopProvider.init.resetHistory();
   1057 
   1058  await feed.refresh();
   1059 
   1060  Assert.ok(
   1061    feed._tippyTopProvider.init.notCalled,
   1062    "tippyTopProvider not initted again"
   1063  );
   1064 
   1065  info("TopSitesFeed.refresh should broadcast TOP_SITES_UPDATED");
   1066  feed.store.dispatch.resetHistory();
   1067  sandbox.stub(feed, "getLinksWithDefaults").resolves([]);
   1068 
   1069  await feed.refresh({ broadcast: true });
   1070 
   1071  Assert.ok(feed.store.dispatch.calledOnce, "dispatch called once");
   1072  Assert.ok(
   1073    feed.store.dispatch.calledWithExactly(
   1074      actionCreators.BroadcastToContent({
   1075        type: actionTypes.TOP_SITES_UPDATED,
   1076        data: { links: [] },
   1077      })
   1078    )
   1079  );
   1080 
   1081  sandbox.restore();
   1082 });
   1083 
   1084 add_task(async function test_refresh_dispatch() {
   1085  let sandbox = sinon.createSandbox();
   1086 
   1087  info(
   1088    "TopSitesFeed.refresh should dispatch an action with the links returned"
   1089  );
   1090 
   1091  let feed = getTopSitesFeedForTest(sandbox);
   1092  sandbox.stub(feed, "_fetchIcon");
   1093  feed._startedUp = true;
   1094 
   1095  await feed.refresh({ broadcast: true });
   1096  let reference = FAKE_LINKS.map(site =>
   1097    Object.assign({}, site, {
   1098      hostname: NewTabUtils.shortURL(site),
   1099      typedBonus: true,
   1100    })
   1101  );
   1102 
   1103  Assert.ok(feed.store.dispatch.calledOnce, "Store.dispatch called once");
   1104  Assert.equal(
   1105    feed.store.dispatch.firstCall.args[0].type,
   1106    actionTypes.TOP_SITES_UPDATED
   1107  );
   1108  Assert.deepEqual(feed.store.dispatch.firstCall.args[0].data.links, reference);
   1109 
   1110  sandbox.restore();
   1111 });
   1112 
   1113 add_task(async function test_refresh_empty_slots() {
   1114  let sandbox = sinon.createSandbox();
   1115 
   1116  info(
   1117    "TopSitesFeed.refresh should handle empty slots in the resulting top sites array"
   1118  );
   1119 
   1120  let feed = getTopSitesFeedForTest(sandbox);
   1121  sandbox.stub(feed, "_fetchIcon");
   1122  feed._startedUp = true;
   1123 
   1124  gGetTopSitesStub.resolves([FAKE_LINKS[0]]);
   1125  sandbox
   1126    .stub(NewTabUtils.pinnedLinks, "links")
   1127    .get(() => [
   1128      null,
   1129      null,
   1130      FAKE_LINKS[1],
   1131      null,
   1132      null,
   1133      null,
   1134      null,
   1135      null,
   1136      FAKE_LINKS[2],
   1137    ]);
   1138 
   1139  await feed.refresh({ broadcast: true });
   1140 
   1141  Assert.ok(feed.store.dispatch.calledOnce, "Store.dispatch called once");
   1142 
   1143  gGetTopSitesStub.resolves(FAKE_LINKS);
   1144  sandbox.restore();
   1145 });
   1146 
   1147 add_task(async function test_refresh_to_preloaded() {
   1148  let sandbox = sinon.createSandbox();
   1149 
   1150  info(
   1151    "TopSitesFeed.refresh should dispatch AlsoToPreloaded when broadcast is false"
   1152  );
   1153 
   1154  let feed = getTopSitesFeedForTest(sandbox);
   1155  sandbox.stub(feed, "_fetchIcon");
   1156  feed._startedUp = true;
   1157 
   1158  gGetTopSitesStub.resolves([]);
   1159  await feed.refresh({ broadcast: false });
   1160 
   1161  Assert.ok(feed.store.dispatch.calledOnce, "Store.dispatch called once");
   1162  Assert.ok(
   1163    feed.store.dispatch.calledWithExactly(
   1164      actionCreators.AlsoToPreloaded({
   1165        type: actionTypes.TOP_SITES_UPDATED,
   1166        data: { links: [] },
   1167      })
   1168    )
   1169  );
   1170  gGetTopSitesStub.resolves(FAKE_LINKS);
   1171  sandbox.restore();
   1172 });
   1173 
   1174 add_task(async function test_refresh_handles_indexedDB_errors() {
   1175  let sandbox = sinon.createSandbox();
   1176 
   1177  info(
   1178    "TopSitesFeed.refresh should dispatch AlsoToPreloaded when broadcast is false"
   1179  );
   1180 
   1181  let feed = getTopSitesFeedForTest(sandbox);
   1182  sandbox.stub(feed, "_fetchIcon");
   1183  feed._startedUp = true;
   1184 
   1185  try {
   1186    await feed.refresh({ broadcast: false });
   1187    Assert.ok(true, "refresh should have succeeded");
   1188  } catch (e) {
   1189    Assert.ok(false, "Should not have thrown");
   1190  }
   1191 
   1192  sandbox.restore();
   1193 });
   1194 
   1195 add_task(async function test_allocatePositions() {
   1196  let sandbox = sinon.createSandbox();
   1197 
   1198  info(
   1199    "TopSitesFeed.allocationPositions should allocate positions and dispatch"
   1200  );
   1201 
   1202  let feed = getTopSitesFeedForTest(sandbox);
   1203 
   1204  let sov = {
   1205    name: "SOV-20230518215316",
   1206    allocations: [
   1207      {
   1208        position: 1,
   1209        allocation: [
   1210          {
   1211            partner: "amp",
   1212            percentage: 100,
   1213          },
   1214          {
   1215            partner: "moz-sales",
   1216            percentage: 0,
   1217          },
   1218        ],
   1219      },
   1220      {
   1221        position: 2,
   1222        allocation: [
   1223          {
   1224            partner: "amp",
   1225            percentage: 80,
   1226          },
   1227          {
   1228            partner: "moz-sales",
   1229            percentage: 20,
   1230          },
   1231        ],
   1232      },
   1233    ],
   1234  };
   1235 
   1236  sandbox.stub(feed._contile, "sov").get(() => sov);
   1237 
   1238  sandbox.stub(Sampling, "ratioSample");
   1239  Sampling.ratioSample.onCall(0).resolves(0);
   1240  Sampling.ratioSample.onCall(1).resolves(1);
   1241 
   1242  await feed.allocatePositions();
   1243 
   1244  Assert.ok(feed.store.dispatch.calledOnce, "feed.store.dispatch called once");
   1245  Assert.ok(
   1246    feed.store.dispatch.calledWithExactly(
   1247      actionCreators.OnlyToMain({
   1248        type: actionTypes.SOV_UPDATED,
   1249        data: {
   1250          ready: true,
   1251          positions: [
   1252            { position: 1, assignedPartner: "amp" },
   1253            { position: 2, assignedPartner: "moz-sales" },
   1254          ],
   1255        },
   1256      })
   1257    )
   1258  );
   1259 
   1260  Sampling.ratioSample.onCall(2).resolves(0);
   1261  Sampling.ratioSample.onCall(3).resolves(0);
   1262 
   1263  await feed.allocatePositions();
   1264 
   1265  Assert.ok(
   1266    feed.store.dispatch.calledTwice,
   1267    "feed.store.dispatch called twice"
   1268  );
   1269  Assert.ok(
   1270    feed.store.dispatch.calledWithExactly(
   1271      actionCreators.OnlyToMain({
   1272        type: actionTypes.SOV_UPDATED,
   1273        data: {
   1274          ready: true,
   1275          positions: [
   1276            { position: 1, assignedPartner: "amp" },
   1277            { position: 2, assignedPartner: "amp" },
   1278          ],
   1279        },
   1280      })
   1281    )
   1282  );
   1283 
   1284  sandbox.restore();
   1285 });
   1286 
   1287 add_task(async function test_getScreenshotPreview() {
   1288  let sandbox = sinon.createSandbox();
   1289 
   1290  info(
   1291    "TopSitesFeed.getScreenshotPreview should dispatch preview if request is succesful"
   1292  );
   1293 
   1294  let feed = getTopSitesFeedForTest(sandbox);
   1295  await feed.getScreenshotPreview("custom", 1234);
   1296 
   1297  Assert.ok(feed.store.dispatch.calledOnce);
   1298  Assert.ok(
   1299    feed.store.dispatch.calledWithExactly(
   1300      actionCreators.OnlyToOneContent(
   1301        {
   1302          data: { preview: FAKE_SCREENSHOT, url: "custom" },
   1303          type: actionTypes.PREVIEW_RESPONSE,
   1304        },
   1305        1234
   1306      )
   1307    )
   1308  );
   1309 
   1310  sandbox.restore();
   1311 });
   1312 
   1313 add_task(async function test_getScreenshotPreview() {
   1314  let sandbox = sinon.createSandbox();
   1315 
   1316  info(
   1317    "TopSitesFeed.getScreenshotPreview should return empty string if request fails"
   1318  );
   1319 
   1320  let feed = getTopSitesFeedForTest(sandbox);
   1321  Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
   1322  await feed.getScreenshotPreview("custom", 1234);
   1323 
   1324  Assert.ok(feed.store.dispatch.calledOnce);
   1325  Assert.ok(
   1326    feed.store.dispatch.calledWithExactly(
   1327      actionCreators.OnlyToOneContent(
   1328        {
   1329          data: { preview: "", url: "custom" },
   1330          type: actionTypes.PREVIEW_RESPONSE,
   1331        },
   1332        1234
   1333      )
   1334    )
   1335  );
   1336 
   1337  Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
   1338  sandbox.restore();
   1339 });
   1340 
   1341 add_task(async function test_onAction_part_1() {
   1342  let sandbox = sinon.createSandbox();
   1343 
   1344  info(
   1345    "TopSitesFeed.onAction should call getScreenshotPreview on PREVIEW_REQUEST"
   1346  );
   1347 
   1348  let feed = getTopSitesFeedForTest(sandbox);
   1349  sandbox.stub(feed, "getScreenshotPreview");
   1350 
   1351  feed.onAction({
   1352    type: actionTypes.PREVIEW_REQUEST,
   1353    data: { url: "foo" },
   1354    meta: { fromTarget: 1234 },
   1355  });
   1356 
   1357  Assert.ok(
   1358    feed.getScreenshotPreview.calledOnce,
   1359    "feed.getScreenshotPreview called once"
   1360  );
   1361  Assert.ok(feed.getScreenshotPreview.calledWithExactly("foo", 1234));
   1362 
   1363  info("TopSitesFeed.onAction should refresh on SYSTEM_TICK");
   1364  sandbox.stub(feed, "refresh");
   1365  feed.onAction({ type: actionTypes.SYSTEM_TICK });
   1366 
   1367  Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
   1368  Assert.ok(feed.refresh.calledWithExactly({ broadcast: false }));
   1369 
   1370  info(
   1371    "TopSitesFeed.onAction should call with correct parameters on TOP_SITES_PIN"
   1372  );
   1373  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1374  sandbox.spy(feed, "pin");
   1375 
   1376  let pinAction = {
   1377    type: actionTypes.TOP_SITES_PIN,
   1378    data: { site: { url: "foo.com" }, index: 7 },
   1379  };
   1380  feed.onAction(pinAction);
   1381  Assert.ok(
   1382    NewTabUtils.pinnedLinks.pin.calledOnce,
   1383    "NewTabUtils.pinnedLinks.pin called once"
   1384  );
   1385  Assert.ok(
   1386    NewTabUtils.pinnedLinks.pin.calledWithExactly(
   1387      pinAction.data.site,
   1388      pinAction.data.index
   1389    )
   1390  );
   1391  Assert.ok(
   1392    feed.pin.calledOnce,
   1393    "TopSitesFeed.onAction should call pin on TOP_SITES_PIN"
   1394  );
   1395 
   1396  info(
   1397    "TopSitesFeed.onAction should unblock a previously blocked top site if " +
   1398      "we are now adding it manually via 'Add a Top Site' option"
   1399  );
   1400  sandbox.stub(NewTabUtils.blockedLinks, "unblock");
   1401  pinAction = {
   1402    type: actionTypes.TOP_SITES_PIN,
   1403    data: { site: { url: "foo.com" }, index: -1 },
   1404  };
   1405  feed.onAction(pinAction);
   1406  Assert.ok(
   1407    NewTabUtils.blockedLinks.unblock.calledWith({
   1408      url: pinAction.data.site.url,
   1409    })
   1410  );
   1411 
   1412  info("TopSitesFeed.onAction should call insert on TOP_SITES_INSERT");
   1413  sandbox.stub(feed, "insert");
   1414  let addAction = {
   1415    type: actionTypes.TOP_SITES_INSERT,
   1416    data: { site: { url: "foo.com" } },
   1417  };
   1418 
   1419  feed.onAction(addAction);
   1420  Assert.ok(feed.insert.calledOnce, "TopSitesFeed.insert called once");
   1421 
   1422  info(
   1423    "TopSitesFeed.onAction should call unpin with correct parameters " +
   1424      "on TOP_SITES_UNPIN"
   1425  );
   1426 
   1427  sandbox
   1428    .stub(NewTabUtils.pinnedLinks, "links")
   1429    .get(() => [
   1430      null,
   1431      null,
   1432      { url: "foo.com" },
   1433      null,
   1434      null,
   1435      null,
   1436      null,
   1437      null,
   1438      FAKE_LINKS[0],
   1439    ]);
   1440  sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
   1441 
   1442  let unpinAction = {
   1443    type: actionTypes.TOP_SITES_UNPIN,
   1444    data: { site: { url: "foo.com" } },
   1445  };
   1446  feed.onAction(unpinAction);
   1447  Assert.ok(
   1448    NewTabUtils.pinnedLinks.unpin.calledOnce,
   1449    "NewTabUtils.pinnedLinks.unpin called once"
   1450  );
   1451  Assert.ok(NewTabUtils.pinnedLinks.unpin.calledWith(unpinAction.data.site));
   1452 
   1453  sandbox.restore();
   1454 });
   1455 
   1456 add_task(async function test_onAction_part_2() {
   1457  let sandbox = sinon.createSandbox();
   1458 
   1459  info(
   1460    "TopSitesFeed.onAction should call refresh without a target if we clear " +
   1461      "history with PLACES_HISTORY_CLEARED"
   1462  );
   1463 
   1464  let feed = getTopSitesFeedForTest(sandbox);
   1465  sandbox.stub(feed, "refresh");
   1466  feed.onAction({ type: actionTypes.PLACES_HISTORY_CLEARED });
   1467 
   1468  Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
   1469  Assert.ok(feed.refresh.calledWithExactly({ broadcast: true }));
   1470 
   1471  feed.refresh.resetHistory();
   1472 
   1473  info(
   1474    "TopSitesFeed.onAction should call refresh without a target " +
   1475      "if we remove a Topsite from history"
   1476  );
   1477  feed.onAction({ type: actionTypes.PLACES_LINKS_DELETED });
   1478 
   1479  Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
   1480  Assert.ok(feed.refresh.calledWithExactly({ broadcast: true }));
   1481 
   1482  info("TopSitesFeed.onAction should call init on INIT action");
   1483  feed.onAction({ type: actionTypes.PLACES_LINKS_DELETED });
   1484  sandbox.stub(feed, "init");
   1485  feed.onAction({ type: actionTypes.INIT });
   1486  Assert.ok(feed.init.calledOnce, "TopSitesFeed.init called once");
   1487 
   1488  info(
   1489    "TopSitesFeed.onAction should call refresh on PLACES_LINK_BLOCKED action"
   1490  );
   1491  feed.refresh.resetHistory();
   1492  await feed.onAction({ type: actionTypes.PLACES_LINK_BLOCKED });
   1493  Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
   1494  Assert.ok(feed.refresh.calledWithExactly({ broadcast: true }));
   1495 
   1496  info(
   1497    "TopSitesFeed.onAction should call refresh on PLACES_LINKS_CHANGED action"
   1498  );
   1499  feed.refresh.resetHistory();
   1500  await feed.onAction({ type: actionTypes.PLACES_LINKS_CHANGED });
   1501  Assert.ok(feed.refresh.calledOnce, "TopSitesFeed.refresh called once");
   1502  Assert.ok(feed.refresh.calledWithExactly({ broadcast: false }));
   1503 
   1504  info(
   1505    "TopSitesFeed.onAction should call pin with correct args on " +
   1506      "TOP_SITES_INSERT without an index specified"
   1507  );
   1508  sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1509 
   1510  let addAction = {
   1511    type: actionTypes.TOP_SITES_INSERT,
   1512    data: { site: { url: "foo.bar", label: "foo" } },
   1513  };
   1514  feed.onAction(addAction);
   1515  Assert.ok(
   1516    NewTabUtils.pinnedLinks.pin.calledOnce,
   1517    "NewTabUtils.pinnedLinks.pin called once"
   1518  );
   1519  Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(addAction.data.site, 0));
   1520 
   1521  info(
   1522    "TopSitesFeed.onAction should call pin with correct args on " +
   1523      "TOP_SITES_INSERT"
   1524  );
   1525  NewTabUtils.pinnedLinks.pin.resetHistory();
   1526  let dropAction = {
   1527    type: actionTypes.TOP_SITES_INSERT,
   1528    data: { site: { url: "foo.bar", label: "foo" }, index: 3 },
   1529  };
   1530  feed.onAction(dropAction);
   1531  Assert.ok(
   1532    NewTabUtils.pinnedLinks.pin.calledOnce,
   1533    "NewTabUtils.pinnedLinks.pin called once"
   1534  );
   1535  Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(dropAction.data.site, 3));
   1536 
   1537  // feed.init needs to actually run in order to register the observers that'll
   1538  // be removed in the following UNINIT test, otherwise uninit will throw.
   1539  feed.init.restore();
   1540  feed.init();
   1541 
   1542  info("TopSitesFeed.onAction should remove the expiration filter on UNINIT");
   1543  sandbox.stub(PageThumbs, "removeExpirationFilter");
   1544  feed.onAction({ type: "UNINIT" });
   1545  Assert.ok(
   1546    PageThumbs.removeExpirationFilter.calledOnce,
   1547    "PageThumbs.removeExpirationFilter called once"
   1548  );
   1549 
   1550  sandbox.restore();
   1551 });
   1552 
   1553 add_task(async function test_onAction_part_3() {
   1554  let sandbox = sinon.createSandbox();
   1555 
   1556  let feed = getTopSitesFeedForTest(sandbox);
   1557 
   1558  info(
   1559    "TopSitesFeed.onAction should call updatePinnedSearchShortcuts " +
   1560      "on UPDATE_PINNED_SEARCH_SHORTCUTS action"
   1561  );
   1562  sandbox.stub(feed, "updatePinnedSearchShortcuts");
   1563  let addedShortcuts = [
   1564    {
   1565      url: "https://google.com",
   1566      searchVendor: "google",
   1567      label: "google",
   1568      searchTopSite: true,
   1569    },
   1570  ];
   1571  await feed.onAction({
   1572    type: actionTypes.UPDATE_PINNED_SEARCH_SHORTCUTS,
   1573    data: { addedShortcuts },
   1574  });
   1575  Assert.ok(
   1576    feed.updatePinnedSearchShortcuts.calledOnce,
   1577    "TopSitesFeed.updatePinnedSearchShortcuts called once"
   1578  );
   1579 
   1580  info(
   1581    "TopSitesFeed.onAction should refresh from Contile on " +
   1582      "SHOW_SPONSORED_PREF if Contile is enabled"
   1583  );
   1584  sandbox.spy(feed._contile, "refresh");
   1585  let prefChangeAction = {
   1586    type: actionTypes.PREF_CHANGED,
   1587    data: { name: SHOW_SPONSORED_PREF },
   1588  };
   1589  sandbox.stub(NimbusFeatures.newtab, "getVariable").returns(true);
   1590  feed.onAction(prefChangeAction);
   1591 
   1592  Assert.ok(
   1593    feed._contile.refresh.calledOnce,
   1594    "TopSitesFeed._contile.refresh called once"
   1595  );
   1596 
   1597  info(
   1598    "TopSitesFeed.onAction should not refresh from Contile on " +
   1599      "SHOW_SPONSORED_PREF if Contile is disabled"
   1600  );
   1601  NimbusFeatures.newtab.getVariable.returns(false);
   1602  feed._contile.refresh.resetHistory();
   1603  feed.onAction(prefChangeAction);
   1604 
   1605  Assert.ok(
   1606    !feed._contile.refresh.calledOnce,
   1607    "TopSitesFeed._contile.refresh never called"
   1608  );
   1609 
   1610  info(
   1611    "TopSitesFeed.onAction should reset Contile cache prefs " +
   1612      "when SHOW_SPONSORED_PREF is false"
   1613  );
   1614  feed._contile.cache.get.returns({ contile: [] });
   1615  Services.prefs.setIntPref(
   1616    CONTILE_CACHE_LAST_FETCH_PREF,
   1617    Math.round(Date.now() / 1000)
   1618  );
   1619  Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 15 * 60);
   1620  prefChangeAction = {
   1621    type: actionTypes.PREF_CHANGED,
   1622    data: { name: SHOW_SPONSORED_PREF, value: false },
   1623  };
   1624  NimbusFeatures.newtab.getVariable.returns(true);
   1625  feed._contile.refresh.resetHistory();
   1626 
   1627  feed.onAction(prefChangeAction);
   1628  Assert.ok(feed._contile.cache.set.calledWith("contile", []));
   1629  Assert.ok(!Services.prefs.prefHasUserValue(CONTILE_CACHE_LAST_FETCH_PREF));
   1630  Assert.ok(
   1631    !Services.prefs.prefHasUserValue(CONTILE_CACHE_VALID_FOR_SECONDS_PREF)
   1632  );
   1633 
   1634  sandbox.restore();
   1635 });
   1636 
   1637 add_task(async function test_insert_part_1() {
   1638  let sandbox = sinon.createSandbox();
   1639 
   1640  let prepFeed = feed => {
   1641    sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1642    return feed;
   1643  };
   1644 
   1645  {
   1646    info(
   1647      "TopSitesFeed.insert should pin site in first slot of empty pinned list"
   1648    );
   1649 
   1650    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1651    Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
   1652    await feed.getScreenshotPreview("custom", 1234);
   1653 
   1654    Assert.ok(feed.store.dispatch.calledOnce);
   1655    Assert.ok(
   1656      feed.store.dispatch.calledWithExactly(
   1657        actionCreators.OnlyToOneContent(
   1658          {
   1659            data: { preview: "", url: "custom" },
   1660            type: actionTypes.PREVIEW_RESPONSE,
   1661          },
   1662          1234
   1663        )
   1664      )
   1665    );
   1666 
   1667    Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
   1668    sandbox.restore();
   1669  }
   1670 
   1671  {
   1672    info(
   1673      "TopSitesFeed.insert should pin site in first slot of pinned list with " +
   1674        "empty first slot"
   1675    );
   1676 
   1677    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1678    sandbox
   1679      .stub(NewTabUtils.pinnedLinks, "links")
   1680      .get(() => [null, { url: "example.com" }]);
   1681    let site = { url: "foo.bar", label: "foo" };
   1682    await feed.insert({ data: { site } });
   1683    Assert.ok(
   1684      NewTabUtils.pinnedLinks.pin.calledOnce,
   1685      "NewTabUtils.pinnedLinks.pin called once"
   1686    );
   1687    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1688    NewTabUtils.pinnedLinks.pin.resetHistory();
   1689    sandbox.restore();
   1690  }
   1691 
   1692  {
   1693    info(
   1694      "TopSitesFeed.insert should move a pinned site in first slot to the " +
   1695        "next slot: part 1"
   1696    );
   1697    let site1 = { url: "example.com" };
   1698    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [site1]);
   1699    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1700    let site = { url: "foo.bar", label: "foo" };
   1701 
   1702    await feed.insert({ data: { site } });
   1703    Assert.ok(
   1704      NewTabUtils.pinnedLinks.pin.calledTwice,
   1705      "NewTabUtils.pinnedLinks.pin called twice"
   1706    );
   1707    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1708    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
   1709    NewTabUtils.pinnedLinks.pin.resetHistory();
   1710    sandbox.restore();
   1711  }
   1712 
   1713  {
   1714    info(
   1715      "TopSitesFeed.insert should move a pinned site in first slot to the " +
   1716        "next slot: part 2"
   1717    );
   1718    let site1 = { url: "example.com" };
   1719    let site2 = { url: "example.org" };
   1720    sandbox
   1721      .stub(NewTabUtils.pinnedLinks, "links")
   1722      .get(() => [site1, null, site2]);
   1723    let site = { url: "foo.bar", label: "foo" };
   1724    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1725    await feed.insert({ data: { site } });
   1726    Assert.ok(
   1727      NewTabUtils.pinnedLinks.pin.calledTwice,
   1728      "NewTabUtils.pinnedLinks.pin called twice"
   1729    );
   1730    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1731    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
   1732    NewTabUtils.pinnedLinks.pin.resetHistory();
   1733    sandbox.restore();
   1734  }
   1735 });
   1736 
   1737 add_task(async function test_insert_part_2() {
   1738  let sandbox = sinon.createSandbox();
   1739 
   1740  let prepFeed = feed => {
   1741    sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1742    return feed;
   1743  };
   1744 
   1745  {
   1746    info(
   1747      "TopSitesFeed.insert should unpin the last site if all slots are " +
   1748        "already pinned"
   1749    );
   1750    let site1 = { url: "example.com" };
   1751    let site2 = { url: "example.org" };
   1752    let site3 = { url: "example.net" };
   1753    let site4 = { url: "example.biz" };
   1754    let site5 = { url: "example.info" };
   1755    let site6 = { url: "example.news" };
   1756    let site7 = { url: "example.lol" };
   1757    let site8 = { url: "example.golf" };
   1758    sandbox
   1759      .stub(NewTabUtils.pinnedLinks, "links")
   1760      .get(() => [site1, site2, site3, site4, site5, site6, site7, site8]);
   1761    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1762    feed.store.state.Prefs.values.topSitesRows = 1;
   1763    let site = { url: "foo.bar", label: "foo" };
   1764    await feed.insert({ data: { site } });
   1765    Assert.equal(
   1766      NewTabUtils.pinnedLinks.pin.callCount,
   1767      8,
   1768      "NewTabUtils.pinnedLinks.pin called 8 times"
   1769    );
   1770    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1771    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
   1772    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 2));
   1773    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site3, 3));
   1774    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site4, 4));
   1775    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site5, 5));
   1776    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site6, 6));
   1777    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site7, 7));
   1778    NewTabUtils.pinnedLinks.pin.resetHistory();
   1779    sandbox.restore();
   1780  }
   1781 
   1782  {
   1783    info("TopSitesFeed.insert should trigger refresh on TOP_SITES_INSERT");
   1784    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1785    sandbox.stub(feed, "refresh");
   1786    let addAction = {
   1787      type: actionTypes.TOP_SITES_INSERT,
   1788      data: { site: { url: "foo.com" } },
   1789    };
   1790 
   1791    await feed.insert(addAction);
   1792 
   1793    Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
   1794    sandbox.restore();
   1795  }
   1796 
   1797  {
   1798    info("TopSitesFeed.insert should correctly handle different index values");
   1799    let index = -1;
   1800    let site = { url: "foo.bar", label: "foo" };
   1801    let action = { data: { index, site } };
   1802    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1803 
   1804    await feed.insert(action);
   1805    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1806 
   1807    index = undefined;
   1808    await feed.insert(action);
   1809    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
   1810 
   1811    NewTabUtils.pinnedLinks.pin.resetHistory();
   1812    sandbox.restore();
   1813  }
   1814 });
   1815 
   1816 add_task(async function test_insert_part_3() {
   1817  let sandbox = sinon.createSandbox();
   1818 
   1819  let prepFeed = feed => {
   1820    sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1821    return feed;
   1822  };
   1823 
   1824  {
   1825    info("TopSitesFeed.insert should pin site in specified slot that is free");
   1826    sandbox
   1827      .stub(NewTabUtils.pinnedLinks, "links")
   1828      .get(() => [null, { url: "example.com" }]);
   1829 
   1830    let site = { url: "foo.bar", label: "foo" };
   1831    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1832 
   1833    await feed.insert({ data: { index: 2, site, draggedFromIndex: 0 } });
   1834    Assert.ok(
   1835      NewTabUtils.pinnedLinks.pin.calledOnce,
   1836      "NewTabUtils.pinnedLinks.pin called once"
   1837    );
   1838    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1839 
   1840    NewTabUtils.pinnedLinks.pin.resetHistory();
   1841    sandbox.restore();
   1842  }
   1843 
   1844  {
   1845    info(
   1846      "TopSitesFeed.insert should move a pinned site in specified slot " +
   1847        "to the next slot"
   1848    );
   1849    sandbox
   1850      .stub(NewTabUtils.pinnedLinks, "links")
   1851      .get(() => [null, null, { url: "example.com" }]);
   1852 
   1853    let site = { url: "foo.bar", label: "foo" };
   1854    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1855 
   1856    await feed.insert({ data: { index: 2, site, draggedFromIndex: 3 } });
   1857    Assert.ok(
   1858      NewTabUtils.pinnedLinks.pin.calledTwice,
   1859      "NewTabUtils.pinnedLinks.pin called twice"
   1860    );
   1861    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1862    Assert.ok(
   1863      NewTabUtils.pinnedLinks.pin.calledWith({ url: "example.com" }, 3)
   1864    );
   1865 
   1866    NewTabUtils.pinnedLinks.pin.resetHistory();
   1867    sandbox.restore();
   1868  }
   1869 
   1870  {
   1871    info(
   1872      "TopSitesFeed.insert should move pinned sites in the direction " +
   1873        "of the dragged site"
   1874    );
   1875 
   1876    let site1 = { url: "foo.bar", label: "foo" };
   1877    let site2 = { url: "example.com", label: "example" };
   1878    sandbox
   1879      .stub(NewTabUtils.pinnedLinks, "links")
   1880      .get(() => [null, null, site2]);
   1881 
   1882    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1883 
   1884    await feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 0 } });
   1885    Assert.ok(
   1886      NewTabUtils.pinnedLinks.pin.calledTwice,
   1887      "NewTabUtils.pinnedLinks.pin called twice"
   1888    );
   1889    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
   1890    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 1));
   1891    NewTabUtils.pinnedLinks.pin.resetHistory();
   1892 
   1893    await feed.insert({ data: { index: 2, site: site1, draggedFromIndex: 5 } });
   1894    Assert.ok(
   1895      NewTabUtils.pinnedLinks.pin.calledTwice,
   1896      "NewTabUtils.pinnedLinks.pin called twice"
   1897    );
   1898    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
   1899    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 3));
   1900    NewTabUtils.pinnedLinks.pin.resetHistory();
   1901    sandbox.restore();
   1902  }
   1903 
   1904  {
   1905    info("TopSitesFeed.insert should not insert past the visible top sites");
   1906 
   1907    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1908    let site1 = { url: "foo.bar", label: "foo" };
   1909    await feed.insert({
   1910      data: { index: 42, site: site1, draggedFromIndex: 0 },
   1911    });
   1912    Assert.ok(
   1913      NewTabUtils.pinnedLinks.pin.notCalled,
   1914      "NewTabUtils.pinnedLinks.pin wasn't called"
   1915    );
   1916 
   1917    NewTabUtils.pinnedLinks.pin.resetHistory();
   1918    sandbox.restore();
   1919  }
   1920 });
   1921 
   1922 add_task(async function test_pin_part_1() {
   1923  let sandbox = sinon.createSandbox();
   1924 
   1925  let prepFeed = feed => {
   1926    sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   1927    return feed;
   1928  };
   1929 
   1930  {
   1931    info(
   1932      "TopSitesFeed.pin should pin site in specified slot empty pinned " +
   1933        "list"
   1934    );
   1935    let site = {
   1936      url: "foo.bar",
   1937      label: "foo",
   1938      customScreenshotURL: "screenshot",
   1939    };
   1940    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1941    await feed.pin({ data: { index: 2, site } });
   1942    Assert.ok(
   1943      NewTabUtils.pinnedLinks.pin.calledOnce,
   1944      "NewTabUtils.pinnedLinks.pin called once"
   1945    );
   1946    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   1947    NewTabUtils.pinnedLinks.pin.resetHistory();
   1948    sandbox.restore();
   1949  }
   1950 
   1951  {
   1952    info(
   1953      "TopSitesFeed.pin should lookup the link object to update the custom " +
   1954        "screenshot"
   1955    );
   1956    let site = {
   1957      url: "foo.bar",
   1958      label: "foo",
   1959      customScreenshotURL: "screenshot",
   1960    };
   1961    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1962    sandbox.spy(feed.pinnedCache, "request");
   1963    await feed.pin({ data: { index: 2, site } });
   1964 
   1965    Assert.ok(
   1966      feed.pinnedCache.request.calledOnce,
   1967      "feed.pinnedCache.request called once"
   1968    );
   1969    NewTabUtils.pinnedLinks.pin.resetHistory();
   1970    sandbox.restore();
   1971  }
   1972 
   1973  {
   1974    info(
   1975      "TopSitesFeed.pin should lookup the link object to update the custom " +
   1976        "screenshot when the custom screenshot is initially null"
   1977    );
   1978    let site = {
   1979      url: "foo.bar",
   1980      label: "foo",
   1981      customScreenshotURL: null,
   1982    };
   1983    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   1984    sandbox.spy(feed.pinnedCache, "request");
   1985    await feed.pin({ data: { index: 2, site } });
   1986 
   1987    Assert.ok(
   1988      feed.pinnedCache.request.calledOnce,
   1989      "feed.pinnedCache.request called once"
   1990    );
   1991    NewTabUtils.pinnedLinks.pin.resetHistory();
   1992    sandbox.restore();
   1993  }
   1994 
   1995  {
   1996    info(
   1997      "TopSitesFeed.pin should not do a link object lookup if custom " +
   1998        "screenshot field is not set"
   1999    );
   2000    let site = { url: "foo.bar", label: "foo" };
   2001    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2002    sandbox.spy(feed.pinnedCache, "request");
   2003    await feed.pin({ data: { index: 2, site } });
   2004 
   2005    Assert.ok(
   2006      !feed.pinnedCache.request.called,
   2007      "feed.pinnedCache.request never called"
   2008    );
   2009    NewTabUtils.pinnedLinks.pin.resetHistory();
   2010    sandbox.restore();
   2011  }
   2012 
   2013  {
   2014    info(
   2015      "TopSitesFeed.pin should pin site in specified slot of pinned " +
   2016        "list that is free"
   2017    );
   2018    sandbox
   2019      .stub(NewTabUtils.pinnedLinks, "links")
   2020      .get(() => [null, { url: "example.com" }]);
   2021 
   2022    let site = { url: "foo.bar", label: "foo" };
   2023    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2024    await feed.pin({ data: { index: 2, site } });
   2025    Assert.ok(
   2026      NewTabUtils.pinnedLinks.pin.calledOnce,
   2027      "NewTabUtils.pinnedLinks.pin called once"
   2028    );
   2029    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   2030    NewTabUtils.pinnedLinks.pin.resetHistory();
   2031    sandbox.restore();
   2032  }
   2033 });
   2034 
   2035 add_task(async function test_pin_part_2() {
   2036  let sandbox = sinon.createSandbox();
   2037 
   2038  let prepFeed = feed => {
   2039    sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   2040    return feed;
   2041  };
   2042 
   2043  {
   2044    info("TopSitesFeed.pin should save the searchTopSite attribute if set");
   2045    sandbox
   2046      .stub(NewTabUtils.pinnedLinks, "links")
   2047      .get(() => [null, { url: "example.com" }]);
   2048 
   2049    let site = { url: "foo.bar", label: "foo", searchTopSite: true };
   2050    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2051    await feed.pin({ data: { index: 2, site } });
   2052    Assert.ok(
   2053      NewTabUtils.pinnedLinks.pin.calledOnce,
   2054      "NewTabUtils.pinnedLinks.pin called once"
   2055    );
   2056    Assert.ok(NewTabUtils.pinnedLinks.pin.firstCall.args[0].searchTopSite);
   2057    NewTabUtils.pinnedLinks.pin.resetHistory();
   2058    sandbox.restore();
   2059  }
   2060 
   2061  {
   2062    info(
   2063      "TopSitesFeed.pin should NOT move a pinned site in specified " +
   2064        "slot to the next slot"
   2065    );
   2066    sandbox
   2067      .stub(NewTabUtils.pinnedLinks, "links")
   2068      .get(() => [null, null, { url: "example.com" }]);
   2069 
   2070    let site = { url: "foo.bar", label: "foo" };
   2071    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2072    await feed.pin({ data: { index: 2, site } });
   2073    Assert.ok(
   2074      NewTabUtils.pinnedLinks.pin.calledOnce,
   2075      "NewTabUtils.pinnedLinks.pin called once"
   2076    );
   2077    Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
   2078    NewTabUtils.pinnedLinks.pin.resetHistory();
   2079    sandbox.restore();
   2080  }
   2081 
   2082  {
   2083    info(
   2084      "TopSitesFeed.pin should properly update LinksCache object " +
   2085        "properties between migrations"
   2086    );
   2087    sandbox
   2088      .stub(NewTabUtils.pinnedLinks, "links")
   2089      .get(() => [{ url: "https://foo.com/" }]);
   2090 
   2091    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2092    let pinnedLinks = await feed.pinnedCache.request();
   2093    Assert.equal(pinnedLinks.length, 1);
   2094    feed.pinnedCache.expire();
   2095 
   2096    pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo");
   2097 
   2098    pinnedLinks = await feed.pinnedCache.request();
   2099    Assert.equal(pinnedLinks[0].screenshot, "foo");
   2100 
   2101    // Force cache expiration in order to trigger a migration of objects
   2102    feed.pinnedCache.expire();
   2103    pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar");
   2104 
   2105    pinnedLinks = await feed.pinnedCache.request();
   2106    Assert.equal(pinnedLinks[0].screenshot, "bar");
   2107    sandbox.restore();
   2108  }
   2109 });
   2110 
   2111 add_task(async function test_pin_part_3() {
   2112  let sandbox = sinon.createSandbox();
   2113 
   2114  let prepFeed = feed => {
   2115    sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   2116    return feed;
   2117  };
   2118 
   2119  {
   2120    info("TopSitesFeed.pin should call insert if index < 0");
   2121    let site = { url: "foo.bar", label: "foo" };
   2122    let action = { data: { index: -1, site } };
   2123    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2124    sandbox.spy(feed, "insert");
   2125    await feed.pin(action);
   2126 
   2127    Assert.ok(feed.insert.calledOnce, "feed.insert called once");
   2128    Assert.ok(feed.insert.calledWithExactly(action));
   2129    NewTabUtils.pinnedLinks.pin.resetHistory();
   2130    sandbox.restore();
   2131  }
   2132 
   2133  {
   2134    info("TopSitesFeed.pin should not call insert if index == 0");
   2135    let site = { url: "foo.bar", label: "foo" };
   2136    let action = { data: { index: 0, site } };
   2137    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2138    sandbox.spy(feed, "insert");
   2139    await feed.pin(action);
   2140 
   2141    Assert.ok(!feed.insert.called, "feed.insert not called");
   2142    NewTabUtils.pinnedLinks.pin.resetHistory();
   2143    sandbox.restore();
   2144  }
   2145 
   2146  {
   2147    info("TopSitesFeed.pin should trigger refresh on TOP_SITES_PIN");
   2148    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2149    sandbox.stub(feed, "refresh");
   2150    let pinExistingAction = {
   2151      type: actionTypes.TOP_SITES_PIN,
   2152      data: { site: FAKE_LINKS[4], index: 4 },
   2153    };
   2154 
   2155    await feed.pin(pinExistingAction);
   2156 
   2157    Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
   2158    NewTabUtils.pinnedLinks.pin.resetHistory();
   2159    sandbox.restore();
   2160  }
   2161 });
   2162 
   2163 add_task(async function test_integration() {
   2164  let sandbox = sinon.createSandbox();
   2165 
   2166  info("Test adding a pinned site and removing it with actions");
   2167  let feed = getTopSitesFeedForTest(sandbox);
   2168 
   2169  let resolvers = [];
   2170  feed.store.dispatch = sandbox.stub().callsFake(() => {
   2171    resolvers.shift()();
   2172  });
   2173  feed._startedUp = true;
   2174  sandbox.stub(feed, "_fetchScreenshot");
   2175 
   2176  let forDispatch = action =>
   2177    new Promise(resolve => {
   2178      resolvers.push(resolve);
   2179      feed.onAction(action);
   2180    });
   2181 
   2182  feed._requestRichIcon = sandbox.stub();
   2183  let url = "https://pin.me";
   2184  sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake(link => {
   2185    NewTabUtils.pinnedLinks.links.push(link);
   2186  });
   2187 
   2188  await forDispatch({
   2189    type: actionTypes.TOP_SITES_INSERT,
   2190    data: { site: { url } },
   2191  });
   2192  NewTabUtils.pinnedLinks.links.pop();
   2193  await forDispatch({ type: actionTypes.PLACES_LINK_BLOCKED });
   2194 
   2195  Assert.ok(
   2196    feed.store.dispatch.calledTwice,
   2197    "feed.store.dispatch called twice"
   2198  );
   2199  Assert.equal(feed.store.dispatch.firstCall.args[0].data.links[0].url, url);
   2200  Assert.equal(
   2201    feed.store.dispatch.secondCall.args[0].data.links[0].url,
   2202    FAKE_LINKS[0].url
   2203  );
   2204 
   2205  sandbox.restore();
   2206 });
   2207 
   2208 add_task(async function test_improvesearch_noDefaultSearchTile_experiment() {
   2209  let sandbox = sinon.createSandbox();
   2210  const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
   2211 
   2212  let prepFeed = feed => {
   2213    sandbox.stub(SearchService.prototype, "getDefault").resolves({
   2214      identifier: "google",
   2215    });
   2216    return feed;
   2217  };
   2218 
   2219  {
   2220    info(
   2221      "TopSitesFeed.getLinksWithDefaults should filter out alexa top 5 " +
   2222        "search from the default sites"
   2223    );
   2224    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2225    feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
   2226    let top5Test = [
   2227      "https://google.com",
   2228      "https://search.yahoo.com",
   2229      "https://yahoo.com",
   2230      "https://bing.com",
   2231      "https://ask.com",
   2232      "https://duckduckgo.com",
   2233    ];
   2234 
   2235    gGetTopSitesStub.resolves([
   2236      { url: "https://amazon.com" },
   2237      ...top5Test.map(url => ({ url })),
   2238    ]);
   2239 
   2240    const urlsReturned = (await feed.getLinksWithDefaults()).map(
   2241      link => link.url
   2242    );
   2243    Assert.ok(
   2244      urlsReturned.includes("https://amazon.com"),
   2245      "amazon included in default links"
   2246    );
   2247    top5Test.forEach(url =>
   2248      Assert.ok(!urlsReturned.includes(url), `Should not include ${url}`)
   2249    );
   2250 
   2251    gGetTopSitesStub.resolves(FAKE_LINKS);
   2252    sandbox.restore();
   2253  }
   2254 
   2255  {
   2256    info(
   2257      "TopSitesFeed.getLinksWithDefaults should not filter out alexa, default " +
   2258        "search from the query results if the experiment pref is off"
   2259    );
   2260    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2261    feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;
   2262 
   2263    gGetTopSitesStub.resolves([
   2264      { url: "https://google.com" },
   2265      { url: "https://foo.com" },
   2266      { url: "https://duckduckgo" },
   2267    ]);
   2268    let urlsReturned = (await feed.getLinksWithDefaults()).map(
   2269      link => link.url
   2270    );
   2271 
   2272    Assert.ok(urlsReturned.includes("https://google.com"));
   2273    gGetTopSitesStub.resolves(FAKE_LINKS);
   2274    sandbox.restore();
   2275  }
   2276 
   2277  {
   2278    info(
   2279      "TopSitesFeed.getLinksWithDefaults should filter out the current " +
   2280        "default search from the default sites"
   2281    );
   2282    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2283    feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
   2284 
   2285    sandbox.stub(feed, "_currentSearchHostname").get(() => "amazon");
   2286    feed.onAction({
   2287      type: actionTypes.PREFS_INITIAL_VALUES,
   2288      data: { "default.sites": "google.com,amazon.com" },
   2289    });
   2290    gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
   2291 
   2292    let urlsReturned = (await feed.getLinksWithDefaults()).map(
   2293      link => link.url
   2294    );
   2295    Assert.ok(!urlsReturned.includes("https://amazon.com"));
   2296 
   2297    gGetTopSitesStub.resolves(FAKE_LINKS);
   2298    sandbox.restore();
   2299  }
   2300 
   2301  {
   2302    info(
   2303      "TopSitesFeed.getLinksWithDefaults should not filter out current " +
   2304        "default search from pinned sites even if it matches the current " +
   2305        "default search"
   2306    );
   2307    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2308    feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
   2309 
   2310    sandbox
   2311      .stub(NewTabUtils.pinnedLinks, "links")
   2312      .get(() => [{ url: "google.com" }]);
   2313    gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
   2314 
   2315    let urlsReturned = (await feed.getLinksWithDefaults()).map(
   2316      link => link.url
   2317    );
   2318    Assert.ok(urlsReturned.includes("google.com"));
   2319 
   2320    gGetTopSitesStub.resolves(FAKE_LINKS);
   2321    sandbox.restore();
   2322  }
   2323 });
   2324 
   2325 add_task(
   2326  async function test_improvesearch_noDefaultSearchTile_experiment_part_2() {
   2327    let sandbox = sinon.createSandbox();
   2328    const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
   2329 
   2330    let prepFeed = feed => {
   2331      sandbox.stub(SearchService.prototype, "getDefault").resolves({
   2332        identifier: "google",
   2333      });
   2334      return feed;
   2335    };
   2336 
   2337    {
   2338      info(
   2339        "TopSitesFeed.getLinksWithDefaults should call refresh and set " +
   2340          "._currentSearchHostname to the new engine hostname when the " +
   2341          "default search engine has been set"
   2342      );
   2343      let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2344      feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
   2345      sandbox.stub(feed, "refresh");
   2346 
   2347      feed.observe(null, "browser-search-engine-modified", "engine-default");
   2348      Assert.equal(feed._currentSearchHostname, "duckduckgo");
   2349      Assert.ok(feed.refresh.calledOnce, "feed.refresh called once");
   2350 
   2351      gGetTopSitesStub.resolves(FAKE_LINKS);
   2352      sandbox.restore();
   2353    }
   2354 
   2355    {
   2356      info(
   2357        "TopSitesFeed.getLinksWithDefaults should call refresh when the " +
   2358          "experiment pref has changed"
   2359      );
   2360      let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2361      feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
   2362      sandbox.stub(feed, "refresh");
   2363 
   2364      feed.onAction({
   2365        type: actionTypes.PREF_CHANGED,
   2366        data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: true },
   2367      });
   2368      Assert.ok(feed.refresh.calledOnce, "feed.refresh was called once");
   2369 
   2370      feed.onAction({
   2371        type: actionTypes.PREF_CHANGED,
   2372        data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: false },
   2373      });
   2374      Assert.ok(feed.refresh.calledTwice, "feed.refresh was called twice");
   2375 
   2376      gGetTopSitesStub.resolves(FAKE_LINKS);
   2377      sandbox.restore();
   2378    }
   2379  }
   2380 );
   2381 
   2382 // eslint-disable-next-line max-statements
   2383 add_task(async function test_improvesearch_topSitesSearchShortcuts() {
   2384  let sandbox = sinon.createSandbox();
   2385  let searchEngines = [{ aliases: ["@google"] }, { aliases: ["@amazon"] }];
   2386 
   2387  let prepFeed = feed => {
   2388    sandbox
   2389      .stub(SearchService.prototype, "getAppProvidedEngines")
   2390      .resolves(searchEngines);
   2391    sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake((site, index) => {
   2392      NewTabUtils.pinnedLinks.links[index] = site;
   2393    });
   2394    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true;
   2395    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] =
   2396      "google,amazon";
   2397    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
   2398    return feed;
   2399  };
   2400 
   2401  {
   2402    info(
   2403      "TopSitesFeed should updateCustomSearchShortcuts when experiment " +
   2404        "pref is turned on"
   2405    );
   2406    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2407    feed.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
   2408    feed.updateCustomSearchShortcuts = sandbox.spy();
   2409 
   2410    // turn the experiment on
   2411    feed.onAction({
   2412      type: actionTypes.PREF_CHANGED,
   2413      data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true },
   2414    });
   2415 
   2416    Assert.ok(
   2417      feed.updateCustomSearchShortcuts.calledOnce,
   2418      "feed.updateCustomSearchShortcuts called once"
   2419    );
   2420    sandbox.restore();
   2421  }
   2422 
   2423  {
   2424    info(
   2425      "TopSitesFeed should filter out default top sites that match a " +
   2426        "hostname of a search shortcut if previously blocked"
   2427    );
   2428    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2429    feed.refreshDefaults("https://amazon.ca");
   2430    sandbox
   2431      .stub(NewTabUtils.blockedLinks, "links")
   2432      .value([{ url: "https://amazon.com" }]);
   2433    sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
   2434      return NewTabUtils.blockedLinks.links[0].url === site.url;
   2435    });
   2436 
   2437    let urlsReturned = (await feed.getLinksWithDefaults()).map(
   2438      link => link.url
   2439    );
   2440    Assert.ok(!urlsReturned.includes("https://amazon.ca"));
   2441    sandbox.restore();
   2442  }
   2443 
   2444  {
   2445    info("TopSitesFeed should update frecent search topsite icon");
   2446    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2447    feed._tippyTopProvider.processSite = site => {
   2448      site.tippyTopIcon = "icon.png";
   2449      site.backgroundColor = "#fff";
   2450      return site;
   2451    };
   2452    gGetTopSitesStub.resolves([{ url: "https://google.com" }]);
   2453 
   2454    let urlsReturned = await feed.getLinksWithDefaults();
   2455 
   2456    let defaultSearchTopsite = urlsReturned.find(
   2457      s => s.url === "https://google.com"
   2458    );
   2459    Assert.ok(defaultSearchTopsite.searchTopSite);
   2460    Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
   2461    Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
   2462    gGetTopSitesStub.resolves(FAKE_LINKS);
   2463    sandbox.restore();
   2464  }
   2465 
   2466  {
   2467    info("TopSitesFeed should update default search topsite icon");
   2468    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2469    feed._tippyTopProvider.processSite = site => {
   2470      site.tippyTopIcon = "icon.png";
   2471      site.backgroundColor = "#fff";
   2472      return site;
   2473    };
   2474    gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
   2475    feed.onAction({
   2476      type: actionTypes.PREFS_INITIAL_VALUES,
   2477      data: { "default.sites": "google.com,amazon.com" },
   2478    });
   2479 
   2480    let urlsReturned = await feed.getLinksWithDefaults();
   2481 
   2482    let defaultSearchTopsite = urlsReturned.find(
   2483      s => s.url === "https://amazon.com"
   2484    );
   2485    Assert.ok(defaultSearchTopsite.searchTopSite);
   2486    Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
   2487    Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
   2488    gGetTopSitesStub.resolves(FAKE_LINKS);
   2489    sandbox.restore();
   2490  }
   2491 
   2492  {
   2493    info(
   2494      "TopSitesFeed should dispatch UPDATE_SEARCH_SHORTCUTS on " +
   2495        "updateCustomSearchShortcuts"
   2496    );
   2497    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2498    feed.store.state.Prefs.values["improvesearch.noDefaultSearchTile"] = true;
   2499    await feed.updateCustomSearchShortcuts();
   2500    Assert.ok(
   2501      feed.store.dispatch.calledOnce,
   2502      "feed.store.dispatch called once"
   2503    );
   2504    Assert.ok(
   2505      feed.store.dispatch.calledWith({
   2506        data: {
   2507          searchShortcuts: [
   2508            {
   2509              keyword: "@google",
   2510              shortURL: "google",
   2511              url: "https://google.com",
   2512              backgroundColor: undefined,
   2513              smallFavicon:
   2514                "chrome://activity-stream/content/data/content/tippytop/favicons/google-com.ico",
   2515              tippyTopIcon:
   2516                "chrome://activity-stream/content/data/content/tippytop/images/google-com@2x.png",
   2517            },
   2518            {
   2519              keyword: "@amazon",
   2520              shortURL: "amazon",
   2521              url: "https://amazon.com",
   2522              backgroundColor: undefined,
   2523              smallFavicon:
   2524                "chrome://activity-stream/content/data/content/tippytop/favicons/amazon.ico",
   2525              tippyTopIcon:
   2526                "chrome://activity-stream/content/data/content/tippytop/images/amazon@2x.png",
   2527            },
   2528          ],
   2529        },
   2530        meta: {
   2531          from: "ActivityStream:Main",
   2532          to: "ActivityStream:Content",
   2533          isStartup: false,
   2534        },
   2535        type: "UPDATE_SEARCH_SHORTCUTS",
   2536      })
   2537    );
   2538  }
   2539 
   2540  sandbox.restore();
   2541 });
   2542 
   2543 // eslint-disable-next-line max-statements
   2544 add_task(async function test_updatePinnedSearchShortcuts() {
   2545  let sandbox = sinon.createSandbox();
   2546 
   2547  let prepFeed = feed => {
   2548    sandbox.stub(NewTabUtils.pinnedLinks, "pin");
   2549    sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
   2550    return feed;
   2551  };
   2552 
   2553  {
   2554    info(
   2555      "TopSitesFeed.updatePinnedSearchShortcuts should unpin a " +
   2556        "shortcut in deletedShortcuts"
   2557    );
   2558    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2559 
   2560    let deletedShortcuts = [
   2561      {
   2562        url: "https://google.com",
   2563        searchVendor: "google",
   2564        label: "google",
   2565        searchTopSite: true,
   2566      },
   2567    ];
   2568    let addedShortcuts = [];
   2569    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
   2570      null,
   2571      null,
   2572      {
   2573        url: "https://amazon.com",
   2574        searchVendor: "amazon",
   2575        label: "amazon",
   2576        searchTopSite: true,
   2577      },
   2578    ]);
   2579 
   2580    feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2581    Assert.ok(
   2582      NewTabUtils.pinnedLinks.pin.notCalled,
   2583      "NewTabUtils.pinnedLinks.pin not called"
   2584    );
   2585    Assert.ok(
   2586      NewTabUtils.pinnedLinks.unpin.calledOnce,
   2587      "NewTabUtils.pinnedLinks.unpin called once"
   2588    );
   2589    Assert.ok(
   2590      NewTabUtils.pinnedLinks.unpin.calledWith({
   2591        url: "https://google.com",
   2592      })
   2593    );
   2594 
   2595    NewTabUtils.pinnedLinks.pin.resetHistory();
   2596    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2597    sandbox.restore();
   2598  }
   2599 
   2600  {
   2601    info(
   2602      "TopSitesFeed.updatePinnedSearchShortcuts should pin a shortcut " +
   2603        "in addedShortcuts"
   2604    );
   2605    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2606 
   2607    let addedShortcuts = [
   2608      {
   2609        url: "https://google.com",
   2610        searchVendor: "google",
   2611        label: "google",
   2612        searchTopSite: true,
   2613      },
   2614    ];
   2615    let deletedShortcuts = [];
   2616    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
   2617      null,
   2618      null,
   2619      {
   2620        url: "https://amazon.com",
   2621        searchVendor: "amazon",
   2622        label: "amazon",
   2623        searchTopSite: true,
   2624      },
   2625    ]);
   2626    feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2627 
   2628    Assert.ok(
   2629      NewTabUtils.pinnedLinks.unpin.notCalled,
   2630      "NewTabUtils.pinnedLinks.unpin not called"
   2631    );
   2632    Assert.ok(
   2633      NewTabUtils.pinnedLinks.pin.calledOnce,
   2634      "NewTabUtils.pinnedLinks.pin called once"
   2635    );
   2636    Assert.ok(
   2637      NewTabUtils.pinnedLinks.pin.calledWith(
   2638        {
   2639          label: "google",
   2640          searchTopSite: true,
   2641          searchVendor: "google",
   2642          url: "https://google.com",
   2643        },
   2644        0
   2645      )
   2646    );
   2647 
   2648    NewTabUtils.pinnedLinks.pin.resetHistory();
   2649    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2650    sandbox.restore();
   2651  }
   2652 
   2653  {
   2654    info(
   2655      "TopSitesFeed.updatePinnedSearchShortcuts should pin and unpin " +
   2656        "in the same action"
   2657    );
   2658    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2659 
   2660    let addedShortcuts = [
   2661      {
   2662        url: "https://google.com",
   2663        searchVendor: "google",
   2664        label: "google",
   2665        searchTopSite: true,
   2666      },
   2667      {
   2668        url: "https://ebay.com",
   2669        searchVendor: "ebay",
   2670        label: "ebay",
   2671        searchTopSite: true,
   2672      },
   2673    ];
   2674    let deletedShortcuts = [
   2675      {
   2676        url: "https://amazon.com",
   2677        searchVendor: "amazon",
   2678        label: "amazon",
   2679        searchTopSite: true,
   2680      },
   2681    ];
   2682 
   2683    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
   2684      { url: "https://foo.com" },
   2685      {
   2686        url: "https://amazon.com",
   2687        searchVendor: "amazon",
   2688        label: "amazon",
   2689        searchTopSite: true,
   2690      },
   2691    ]);
   2692 
   2693    feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2694 
   2695    Assert.ok(
   2696      NewTabUtils.pinnedLinks.unpin.calledOnce,
   2697      "NewTabUtils.pinnedLinks.unpin called once"
   2698    );
   2699    Assert.ok(
   2700      NewTabUtils.pinnedLinks.pin.calledTwice,
   2701      "NewTabUtils.pinnedLinks.pin called twice"
   2702    );
   2703 
   2704    NewTabUtils.pinnedLinks.pin.resetHistory();
   2705    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2706    sandbox.restore();
   2707  }
   2708 
   2709  {
   2710    info(
   2711      "TopSitesFeed.updatePinnedSearchShortcuts should pin a shortcut in " +
   2712        "addedShortcuts even if pinnedLinks is full"
   2713    );
   2714    let feed = prepFeed(getTopSitesFeedForTest(sandbox));
   2715 
   2716    let addedShortcuts = [
   2717      {
   2718        url: "https://google.com",
   2719        searchVendor: "google",
   2720        label: "google",
   2721        searchTopSite: true,
   2722      },
   2723    ];
   2724    let deletedShortcuts = [];
   2725    sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => FAKE_LINKS);
   2726    feed.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
   2727 
   2728    Assert.ok(
   2729      NewTabUtils.pinnedLinks.unpin.notCalled,
   2730      "NewTabUtils.pinnedLinks.unpin not called"
   2731    );
   2732    Assert.ok(
   2733      NewTabUtils.pinnedLinks.pin.calledWith(
   2734        { label: "google", searchTopSite: true, url: "https://google.com" },
   2735        0
   2736      ),
   2737      "NewTabUtils.pinnedLinks.unpin not called"
   2738    );
   2739 
   2740    NewTabUtils.pinnedLinks.pin.resetHistory();
   2741    NewTabUtils.pinnedLinks.unpin.resetHistory();
   2742    sandbox.restore();
   2743  }
   2744 });
   2745 
   2746 // eslint-disable-next-line max-statements
   2747 add_task(async function test_ContileIntegration() {
   2748  let sandbox = sinon.createSandbox();
   2749  Services.prefs.setStringPref(
   2750    TOP_SITES_BLOCKED_SPONSORS_PREF,
   2751    `["foo","bar"]`
   2752  );
   2753 
   2754  let prepFeed = feed => {
   2755    sandbox.stub(NimbusFeatures.newtab, "getVariable").returns(true);
   2756    feed.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true;
   2757    let fetchStub = sandbox.stub(feed, "fetch");
   2758    return { feed, fetchStub };
   2759  };
   2760 
   2761  {
   2762    info("TopSitesFeed._fetchSites should fetch sites from Contile");
   2763    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   2764    fetchStub.resolves({
   2765      ok: true,
   2766      status: 200,
   2767      headers: new Map([
   2768        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   2769      ]),
   2770      json: () =>
   2771        Promise.resolve({
   2772          tiles: [
   2773            {
   2774              url: "https://www.test.com",
   2775              image_url: "images/test-com.png",
   2776              click_url: "https://www.test-click.com",
   2777              impression_url: "https://www.test-impression.com",
   2778              name: "test",
   2779            },
   2780            {
   2781              url: "https://www.test1.com",
   2782              image_url: "images/test1-com.png",
   2783              click_url: "https://www.test1-click.com",
   2784              impression_url: "https://www.test1-impression.com",
   2785              name: "test1",
   2786            },
   2787          ],
   2788        }),
   2789    });
   2790 
   2791    let fetched = await feed._contile._fetchSites();
   2792 
   2793    Assert.ok(fetched);
   2794    Assert.equal(feed._contile.sites.length, 2);
   2795 
   2796    info("TopSitesFeed._fetchSites should not send cookies");
   2797    Assert.ok(fetchStub.calledOnce, "fetch should be called once");
   2798    Assert.equal(
   2799      fetchStub.firstCall.args[1].credentials,
   2800      "omit",
   2801      "should not send cookies"
   2802    );
   2803    sandbox.restore();
   2804  }
   2805 
   2806  {
   2807    info("TopSitesFeed._fetchSites should call allocatePositions");
   2808    let { feed } = prepFeed(getTopSitesFeedForTest(sandbox));
   2809    sandbox.stub(feed, "allocatePositions").resolves();
   2810    await feed._contile.refresh();
   2811 
   2812    Assert.ok(
   2813      feed.allocatePositions.calledOnce,
   2814      "feed.allocatePositions called once"
   2815    );
   2816    sandbox.restore();
   2817  }
   2818 
   2819  {
   2820    info(
   2821      "TopSitesFeed._fetchSites should fetch SOV (Share-of-Voice) " +
   2822        "settings from Contile"
   2823    );
   2824    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   2825 
   2826    let sov = {
   2827      name: "SOV-20230518215316",
   2828      allocations: [
   2829        {
   2830          position: 1,
   2831          allocation: [
   2832            {
   2833              partner: "foo",
   2834              percentage: 100,
   2835            },
   2836            {
   2837              partner: "bar",
   2838              percentage: 0,
   2839            },
   2840          ],
   2841        },
   2842        {
   2843          position: 2,
   2844          allocation: [
   2845            {
   2846              partner: "foo",
   2847              percentage: 80,
   2848            },
   2849            {
   2850              partner: "bar",
   2851              percentage: 20,
   2852            },
   2853          ],
   2854        },
   2855      ],
   2856    };
   2857    fetchStub.resolves({
   2858      ok: true,
   2859      status: 200,
   2860      headers: new Map([
   2861        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   2862      ]),
   2863      json: () =>
   2864        Promise.resolve({
   2865          sov: btoa(JSON.stringify(sov)),
   2866          tiles: [
   2867            {
   2868              url: "https://www.test.com",
   2869              image_url: "images/test-com.png",
   2870              click_url: "https://www.test-click.com",
   2871              impression_url: "https://www.test-impression.com",
   2872              name: "test",
   2873            },
   2874            {
   2875              url: "https://www.test1.com",
   2876              image_url: "images/test1-com.png",
   2877              click_url: "https://www.test1-click.com",
   2878              impression_url: "https://www.test1-impression.com",
   2879              name: "test1",
   2880            },
   2881          ],
   2882        }),
   2883    });
   2884 
   2885    let fetched = await feed._contile._fetchSites();
   2886 
   2887    Assert.ok(fetched);
   2888    Assert.deepEqual(feed._contile.sov, sov);
   2889    Assert.equal(feed._contile.sites.length, 2);
   2890    sandbox.restore();
   2891  }
   2892 
   2893  {
   2894    info(
   2895      "TopSitesFeed._fetchSites should not fetch from Contile if " +
   2896        "it's not enabled"
   2897    );
   2898    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   2899 
   2900    NimbusFeatures.newtab.getVariable.reset();
   2901    NimbusFeatures.newtab.getVariable.returns(false);
   2902    let fetched = await feed._contile._fetchSites();
   2903 
   2904    Assert.ok(fetchStub.notCalled, "TopSitesFeed.fetch was not called");
   2905    Assert.ok(!fetched);
   2906    Assert.equal(feed._contile.sites.length, 0);
   2907    sandbox.restore();
   2908  }
   2909 
   2910  {
   2911    info(
   2912      "TopSitesFeed._fetchSites should still return two tiles when Contile " +
   2913        "provides more than 2 tiles and filtering results in more than 2 tiles"
   2914    );
   2915    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   2916 
   2917    NimbusFeatures.newtab.getVariable.reset();
   2918    NimbusFeatures.newtab.getVariable.onCall(0).returns(true);
   2919    NimbusFeatures.newtab.getVariable.onCall(1).returns(true);
   2920 
   2921    fetchStub.resolves({
   2922      ok: true,
   2923      status: 200,
   2924      headers: new Map([
   2925        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   2926      ]),
   2927      json: () =>
   2928        Promise.resolve({
   2929          tiles: [
   2930            {
   2931              url: "https://www.test.com",
   2932              image_url: "images/test-com.png",
   2933              click_url: "https://www.test-click.com",
   2934              impression_url: "https://www.test-impression.com",
   2935              name: "test",
   2936            },
   2937            {
   2938              url: "https://foo.com",
   2939              image_url: "images/foo-com.png",
   2940              click_url: "https://www.foo-click.com",
   2941              impression_url: "https://www.foo-impression.com",
   2942              name: "foo",
   2943            },
   2944            {
   2945              url: "https://bar.com",
   2946              image_url: "images/bar-com.png",
   2947              click_url: "https://www.bar-click.com",
   2948              impression_url: "https://www.bar-impression.com",
   2949              name: "bar",
   2950            },
   2951            {
   2952              url: "https://test1.com",
   2953              image_url: "images/test1-com.png",
   2954              click_url: "https://www.test1-click.com",
   2955              impression_url: "https://www.test1-impression.com",
   2956              name: "test1",
   2957            },
   2958            {
   2959              url: "https://test2.com",
   2960              image_url: "images/test2-com.png",
   2961              click_url: "https://www.test2-click.com",
   2962              impression_url: "https://www.test2-impression.com",
   2963              name: "test2",
   2964            },
   2965          ],
   2966        }),
   2967    });
   2968 
   2969    let fetched = await feed._contile._fetchSites();
   2970 
   2971    Assert.ok(fetched);
   2972    // Both "foo" and "bar" should be filtered
   2973    Assert.equal(feed._contile.sites.length, 3);
   2974    Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
   2975    Assert.equal(feed._contile.sites[1].url, "https://test1.com");
   2976    Assert.equal(feed._contile.sites[2].url, "https://test2.com");
   2977    sandbox.restore();
   2978  }
   2979 
   2980  {
   2981    info(
   2982      "TopSitesFeed._fetchSites should still return two tiles with " +
   2983        "replacement if the Nimbus variable was unset"
   2984    );
   2985    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   2986 
   2987    NimbusFeatures.newtab.getVariable.reset();
   2988    NimbusFeatures.newtab.getVariable.onCall(0).returns(true);
   2989    NimbusFeatures.newtab.getVariable.onCall(1).returns(undefined);
   2990 
   2991    fetchStub.resolves({
   2992      ok: true,
   2993      status: 200,
   2994      headers: new Map([
   2995        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   2996      ]),
   2997      json: () =>
   2998        Promise.resolve({
   2999          tiles: [
   3000            {
   3001              url: "https://www.test.com",
   3002              image_url: "images/test-com.png",
   3003              click_url: "https://www.test-click.com",
   3004              impression_url: "https://www.test-impression.com",
   3005              name: "test",
   3006            },
   3007            {
   3008              url: "https://foo.com",
   3009              image_url: "images/foo-com.png",
   3010              click_url: "https://www.foo-click.com",
   3011              impression_url: "https://www.foo-impression.com",
   3012              name: "foo",
   3013            },
   3014            {
   3015              url: "https://test1.com",
   3016              image_url: "images/test1-com.png",
   3017              click_url: "https://www.test1-click.com",
   3018              impression_url: "https://www.test1-impression.com",
   3019              name: "test1",
   3020            },
   3021          ],
   3022        }),
   3023    });
   3024 
   3025    let fetched = await feed._contile._fetchSites();
   3026 
   3027    Assert.ok(fetched);
   3028    Assert.equal(feed._contile.sites.length, 2);
   3029    Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
   3030    Assert.equal(feed._contile.sites[1].url, "https://test1.com");
   3031    sandbox.restore();
   3032  }
   3033 
   3034  {
   3035    info("TopSitesFeed._fetchSites should filter the blocked sponsors");
   3036 
   3037    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3038    NimbusFeatures.newtab.getVariable.returns(true);
   3039 
   3040    fetchStub.resolves({
   3041      ok: true,
   3042      status: 200,
   3043      headers: new Map([
   3044        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   3045      ]),
   3046      json: () =>
   3047        Promise.resolve({
   3048          tiles: [
   3049            {
   3050              url: "https://www.test.com",
   3051              image_url: "images/test-com.png",
   3052              click_url: "https://www.test-click.com",
   3053              impression_url: "https://www.test-impression.com",
   3054              name: "test",
   3055            },
   3056            {
   3057              url: "https://foo.com",
   3058              image_url: "images/foo-com.png",
   3059              click_url: "https://www.foo-click.com",
   3060              impression_url: "https://www.foo-impression.com",
   3061              name: "foo",
   3062            },
   3063            {
   3064              url: "https://bar.com",
   3065              image_url: "images/bar-com.png",
   3066              click_url: "https://www.bar-click.com",
   3067              impression_url: "https://www.bar-impression.com",
   3068              name: "bar",
   3069            },
   3070          ],
   3071        }),
   3072    });
   3073 
   3074    let fetched = await feed._contile._fetchSites();
   3075 
   3076    Assert.ok(fetched);
   3077    // Both "foo" and "bar" should be filtered
   3078    Assert.equal(feed._contile.sites.length, 1);
   3079    Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
   3080    sandbox.restore();
   3081  }
   3082 
   3083  {
   3084    info(
   3085      "TopSitesFeed._fetchSites should return false when Contile returns " +
   3086        "with error status and no values are stored in cache prefs"
   3087    );
   3088    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3089    NimbusFeatures.newtab.getVariable.returns(true);
   3090    feed._contile.cache.get.returns({ contile: [] });
   3091    Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, 0);
   3092 
   3093    fetchStub.resolves({
   3094      ok: false,
   3095      status: 500,
   3096    });
   3097 
   3098    let fetched = await feed._contile._fetchSites();
   3099 
   3100    Assert.ok(!fetched);
   3101    Assert.ok(!feed._contile.sites.length);
   3102    sandbox.restore();
   3103  }
   3104 
   3105  {
   3106    info(
   3107      "TopSitesFeed._fetchSites should return false when Contile " +
   3108        "returns with error status and cached tiles are expried"
   3109    );
   3110    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3111    NimbusFeatures.newtab.getVariable.returns(true);
   3112    feed._contile.cache.get.returns({ contile: [] });
   3113    const THIRTY_MINUTES_AGO_IN_SECONDS =
   3114      Math.round(Date.now() / 1000) - 60 * 30;
   3115    Services.prefs.setIntPref(
   3116      CONTILE_CACHE_LAST_FETCH_PREF,
   3117      THIRTY_MINUTES_AGO_IN_SECONDS
   3118    );
   3119    Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
   3120 
   3121    fetchStub.resolves({
   3122      ok: false,
   3123      status: 500,
   3124    });
   3125 
   3126    let fetched = await feed._contile._fetchSites();
   3127 
   3128    Assert.ok(!fetched);
   3129    Assert.ok(!feed._contile.sites.length);
   3130    sandbox.restore();
   3131  }
   3132 
   3133  {
   3134    info(
   3135      "TopSitesFeed._fetchSites should handle invalid payload " +
   3136        "properly from Contile"
   3137    );
   3138    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3139 
   3140    NimbusFeatures.newtab.getVariable.returns(true);
   3141    fetchStub.resolves({
   3142      ok: true,
   3143      status: 200,
   3144      json: () =>
   3145        Promise.resolve({
   3146          unknown: [],
   3147        }),
   3148    });
   3149 
   3150    let fetched = await feed._contile._fetchSites();
   3151 
   3152    Assert.ok(!fetched);
   3153    Assert.ok(!feed._contile.sites.length);
   3154    sandbox.restore();
   3155  }
   3156 
   3157  {
   3158    info(
   3159      "TopSitesFeed._fetchSites should handle empty payload properly " +
   3160        "from Contile"
   3161    );
   3162    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3163    NimbusFeatures.newtab.getVariable.returns(true);
   3164 
   3165    fetchStub.resolves({
   3166      ok: true,
   3167      status: 200,
   3168      headers: new Map([
   3169        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   3170      ]),
   3171      json: () =>
   3172        Promise.resolve({
   3173          tiles: [],
   3174        }),
   3175    });
   3176 
   3177    let fetched = await feed._contile._fetchSites();
   3178 
   3179    Assert.ok(fetched);
   3180    Assert.ok(!feed._contile.sites.length);
   3181    sandbox.restore();
   3182  }
   3183 
   3184  {
   3185    info(
   3186      "TopSitesFeed._fetchSites should handle no content properly " +
   3187        "from Contile"
   3188    );
   3189    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3190    NimbusFeatures.newtab.getVariable.returns(true);
   3191 
   3192    fetchStub.resolves({ ok: true, status: 204 });
   3193 
   3194    let fetched = await feed._contile._fetchSites();
   3195 
   3196    Assert.ok(!fetched);
   3197    Assert.ok(!feed._contile.sites.length);
   3198    sandbox.restore();
   3199  }
   3200 
   3201  {
   3202    info(
   3203      "TopSitesFeed._fetchSites should set Caching Prefs after " +
   3204        "a successful request"
   3205    );
   3206    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3207    NimbusFeatures.newtab.getVariable.returns(true);
   3208 
   3209    let tiles = [
   3210      {
   3211        url: "https://www.test.com",
   3212        image_url: "images/test-com.png",
   3213        click_url: "https://www.test-click.com",
   3214        impression_url: "https://www.test-impression.com",
   3215        name: "test",
   3216      },
   3217      {
   3218        url: "https://www.test1.com",
   3219        image_url: "images/test1-com.png",
   3220        click_url: "https://www.test1-click.com",
   3221        impression_url: "https://www.test1-impression.com",
   3222        name: "test1",
   3223      },
   3224    ];
   3225    fetchStub.resolves({
   3226      ok: true,
   3227      status: 200,
   3228      headers: new Map([
   3229        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   3230      ]),
   3231      json: () =>
   3232        Promise.resolve({
   3233          tiles,
   3234        }),
   3235    });
   3236 
   3237    let fetched = await feed._contile._fetchSites();
   3238    Assert.ok(fetched);
   3239    Assert.ok(feed._contile.cache.set.calledWith("contile", tiles));
   3240    Assert.equal(
   3241      Services.prefs.getIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF),
   3242      11322
   3243    );
   3244    sandbox.restore();
   3245  }
   3246 
   3247  {
   3248    info(
   3249      "TopSitesFeed._fetchSites should return cached valid tiles " +
   3250        "when Contile returns error status"
   3251    );
   3252    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3253    NimbusFeatures.newtab.getVariable.returns(true);
   3254 
   3255    let tiles = [
   3256      {
   3257        url: "https://www.test-cached.com",
   3258        image_url: "images/test-com.png",
   3259        click_url: "https://www.test-click.com",
   3260        impression_url: "https://www.test-impression.com",
   3261        name: "test",
   3262      },
   3263      {
   3264        url: "https://www.test1-cached.com",
   3265        image_url: "images/test1-com.png",
   3266        click_url: "https://www.test1-click.com",
   3267        impression_url: "https://www.test1-impression.com",
   3268        name: "test1",
   3269      },
   3270    ];
   3271 
   3272    feed._contile.cache.get.returns({ contile: tiles });
   3273    Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
   3274    Services.prefs.setIntPref(
   3275      CONTILE_CACHE_LAST_FETCH_PREF,
   3276      Math.round(Date.now() / 1000)
   3277    );
   3278 
   3279    fetchStub.resolves({
   3280      status: 304,
   3281    });
   3282 
   3283    let fetched = await feed._contile._fetchSites();
   3284    Assert.ok(fetched);
   3285    Assert.equal(feed._contile.sites.length, 2);
   3286    Assert.equal(feed._contile.sites[0].url, "https://www.test-cached.com");
   3287    Assert.equal(feed._contile.sites[1].url, "https://www.test1-cached.com");
   3288    sandbox.restore();
   3289  }
   3290 
   3291  {
   3292    info(
   3293      "TopSitesFeed._fetchSites should not be successful when contile " +
   3294        "returns an error and no valid tiles are cached"
   3295    );
   3296    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3297    NimbusFeatures.newtab.getVariable.returns(true);
   3298 
   3299    feed._contile.cache.get.returns({ contile: [] });
   3300    Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 0);
   3301    Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, 0);
   3302 
   3303    fetchStub.resolves({
   3304      status: 500,
   3305    });
   3306 
   3307    let fetched = await feed._contile._fetchSites();
   3308    Assert.ok(!fetched);
   3309    sandbox.restore();
   3310  }
   3311 
   3312  {
   3313    info(
   3314      "TopSitesFeed._fetchSites should return cached valid tiles " +
   3315        "filtering blocked tiles when Contile returns error status"
   3316    );
   3317    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3318    NimbusFeatures.newtab.getVariable.returns(true);
   3319 
   3320    let tiles = [
   3321      {
   3322        url: "https://foo.com",
   3323        image_url: "images/foo-com.png",
   3324        click_url: "https://www.foo-click.com",
   3325        impression_url: "https://www.foo-impression.com",
   3326        name: "foo",
   3327      },
   3328      {
   3329        url: "https://www.test1-cached.com",
   3330        image_url: "images/test1-com.png",
   3331        click_url: "https://www.test1-click.com",
   3332        impression_url: "https://www.test1-impression.com",
   3333        name: "test1",
   3334      },
   3335    ];
   3336    feed._contile.cache.get.returns({ contile: tiles });
   3337    Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
   3338    Services.prefs.setIntPref(
   3339      CONTILE_CACHE_LAST_FETCH_PREF,
   3340      Math.round(Date.now() / 1000)
   3341    );
   3342 
   3343    fetchStub.resolves({
   3344      status: 304,
   3345    });
   3346 
   3347    let fetched = await feed._contile._fetchSites();
   3348    Assert.ok(fetched);
   3349    Assert.equal(feed._contile.sites.length, 1);
   3350    Assert.equal(feed._contile.sites[0].url, "https://www.test1-cached.com");
   3351    sandbox.restore();
   3352  }
   3353 
   3354  {
   3355    info(
   3356      "TopSitesFeed._fetchSites should still return 3 tiles when nimbus " +
   3357        "variable overrides max num of sponsored contile tiles"
   3358    );
   3359    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3360    NimbusFeatures.newtab.getVariable.returns(true);
   3361 
   3362    sandbox.stub(NimbusFeatures.pocketNewtab, "getVariable").returns(3);
   3363    fetchStub.resolves({
   3364      ok: true,
   3365      status: 200,
   3366      headers: new Map([
   3367        ["cache-control", "private, max-age=859, stale-if-error=10463"],
   3368      ]),
   3369      json: () =>
   3370        Promise.resolve({
   3371          tiles: [
   3372            {
   3373              url: "https://www.test.com",
   3374              image_url: "images/test-com.png",
   3375              click_url: "https://www.test-click.com",
   3376              impression_url: "https://www.test-impression.com",
   3377              name: "test",
   3378            },
   3379            {
   3380              url: "https://test1.com",
   3381              image_url: "images/test1-com.png",
   3382              click_url: "https://www.test1-click.com",
   3383              impression_url: "https://www.test1-impression.com",
   3384              name: "test1",
   3385            },
   3386            {
   3387              url: "https://test2.com",
   3388              image_url: "images/test2-com.png",
   3389              click_url: "https://www.test2-click.com",
   3390              impression_url: "https://www.test2-impression.com",
   3391              name: "test2",
   3392            },
   3393          ],
   3394        }),
   3395    });
   3396 
   3397    let fetched = await feed._contile._fetchSites();
   3398 
   3399    Assert.ok(fetched);
   3400    Assert.equal(feed._contile.sites.length, 3);
   3401    Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
   3402    Assert.equal(feed._contile.sites[1].url, "https://test1.com");
   3403    Assert.equal(feed._contile.sites[2].url, "https://test2.com");
   3404    sandbox.restore();
   3405  }
   3406 
   3407  {
   3408    info(
   3409      "TopSitesFeed._fetchSites should cast headers from a Headers object to JS object when using OHTTP"
   3410    );
   3411    let { feed, fetchStub } = prepFeed(getTopSitesFeedForTest(sandbox));
   3412 
   3413    Services.prefs.setStringPref(
   3414      "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL",
   3415      "https://relay.url"
   3416    );
   3417    Services.prefs.setStringPref(
   3418      "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL",
   3419      "https://config.url"
   3420    );
   3421    Services.prefs.setBoolPref(
   3422      "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled",
   3423      true
   3424    );
   3425    feed.store.state.Prefs.values["unifiedAds.tiles.enabled"] = true;
   3426    feed.store.state.Prefs.values["unifiedAds.adsFeed.enabled"] = false;
   3427    feed.store.state.Prefs.values["unifiedAds.endpoint"] =
   3428      "https://test.endpoint/";
   3429    feed.store.state.Prefs.values["discoverystream.placements.tiles"] = "1";
   3430    feed.store.state.Prefs.values["discoverystream.placements.tiles.counts"] =
   3431      "1";
   3432    feed.store.state.Prefs.values["unifiedAds.blockedAds"] = "";
   3433 
   3434    const TEST_PREFLIGHT_UA_STRING = "Some test UA";
   3435    const TEST_PREFLIGHT_GEONAME_ID = "Some geo name";
   3436    const TEST_PREFLIGHT_GEO_LOCATION = "Some geo location";
   3437 
   3438    fetchStub.resolves({
   3439      ok: true,
   3440      status: 200,
   3441      json: () =>
   3442        Promise.resolve({
   3443          normalized_ua: TEST_PREFLIGHT_UA_STRING,
   3444          geoname_id: TEST_PREFLIGHT_GEONAME_ID,
   3445          geo_location: TEST_PREFLIGHT_GEO_LOCATION,
   3446        }),
   3447    });
   3448 
   3449    const fakeOhttpConfig = { config: "config" };
   3450    sandbox.stub(ObliviousHTTP, "getOHTTPConfig").resolves(fakeOhttpConfig);
   3451 
   3452    const ohttpRequestStub = sandbox
   3453      .stub(ObliviousHTTP, "ohttpRequest")
   3454      .resolves({
   3455        ok: true,
   3456        status: 200,
   3457        headers: new Map([
   3458          ["cache-control", "private, max-age=859, stale-if-error=10463"],
   3459        ]),
   3460        json: () =>
   3461          Promise.resolve({
   3462            1: [
   3463              {
   3464                block_key: 12345,
   3465                name: "test",
   3466                url: "https://www.test.com",
   3467                image_url: "images/test-com.png",
   3468                callbacks: {
   3469                  click: "https://www.test-click.com",
   3470                  impression: "https://www.test-impression.com",
   3471                },
   3472              },
   3473            ],
   3474          }),
   3475      });
   3476 
   3477    let fetched = await feed._contile._fetchSites();
   3478 
   3479    Assert.ok(fetchStub.calledOnce, "The preflight request was made.");
   3480 
   3481    Assert.ok(fetched);
   3482    Assert.ok(
   3483      ohttpRequestStub.calledOnce,
   3484      "ohttpRequest should be called once"
   3485    );
   3486    const callArgs = ohttpRequestStub.getCall(0).args;
   3487    Assert.equal(callArgs[0], "https://relay.url", "relay URL should match");
   3488    Assert.deepEqual(
   3489      callArgs[1],
   3490      fakeOhttpConfig,
   3491      "config should be passed through"
   3492    );
   3493 
   3494    const sentHeaders = callArgs[3].headers;
   3495    Assert.equal(
   3496      typeof sentHeaders,
   3497      "object",
   3498      "headers should be a plain object"
   3499    );
   3500    Assert.ok(
   3501      // We use instanceof here since isInstance isn't available for
   3502      // Headers, it seems.
   3503      // eslint-disable-next-line mozilla/use-isInstance
   3504      !(sentHeaders instanceof Headers),
   3505      "headers should not be a Headers instance"
   3506    );
   3507 
   3508    Assert.equal(
   3509      sentHeaders["x-user-agent"],
   3510      TEST_PREFLIGHT_UA_STRING,
   3511      "Sent the x-user-agent header from preflight"
   3512    );
   3513    Assert.equal(
   3514      sentHeaders["x-geoname-id"],
   3515      TEST_PREFLIGHT_GEONAME_ID,
   3516      "Sent the x-geoname-id header from preflight"
   3517    );
   3518    Assert.equal(
   3519      sentHeaders["x-geo-location"],
   3520      TEST_PREFLIGHT_GEO_LOCATION,
   3521      "Sent the x-geo-location header from preflight"
   3522    );
   3523 
   3524    info("TopSitesFeed._fetchSites should not send cookies via OHTTP");
   3525    Assert.equal(callArgs[3].credentials, "omit", "should not send cookies");
   3526 
   3527    Services.prefs.clearUserPref(
   3528      "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL"
   3529    );
   3530    Services.prefs.clearUserPref(
   3531      "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL"
   3532    );
   3533    Services.prefs.clearUserPref(
   3534      "browser.newtabpage.activity-stream.unifiedAds.ohttp.enabled"
   3535    );
   3536    sandbox.restore();
   3537  }
   3538 
   3539  Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
   3540 });